From af3c7cb4c4e5a80e5a2e79280a3a61ac66ce7605 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Thu, 25 Apr 2024 16:22:12 +0200 Subject: [PATCH 01/17] added i18n packages and basic configuration, created assets folder in src --- package.json | 12 ++++++---- src/{ => assets}/Map.svg | 0 src/{ => assets}/favicon.svg | 0 src/assets/i18n/locales/en/translation.json | 7 ++++++ src/assets/i18n/locales/fr/translation.json | 7 ++++++ src/{ => assets}/logo.svg | 0 src/components/Header/Header.js | 2 +- src/i18n.js | 26 +++++++++++++++++++++ src/index.js | 1 + src/pages/home/Home.js | 4 ++++ 10 files changed, 53 insertions(+), 6 deletions(-) rename src/{ => assets}/Map.svg (100%) rename src/{ => assets}/favicon.svg (100%) create mode 100644 src/assets/i18n/locales/en/translation.json create mode 100644 src/assets/i18n/locales/fr/translation.json rename src/{ => assets}/logo.svg (100%) create mode 100644 src/i18n.js diff --git a/package.json b/package.json index c706491..536d40d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "react-hanger": "^2.2.1", "react-html-parser": "^2.0.2", "react-router-dom": "^5.2.0", - "react-scripts": "^3.3.0" + "react-scripts": "^3.3.0", + "i18next": "^23.11.2", + "react-i18next": "^14.1.1" }, "scripts": { "start": "react-scripts start", @@ -45,12 +47,12 @@ ] }, "devDependencies": { - "jscs": "^3.0.7", + "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", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3" + "prettier": "^3.2.5" }, "husky": { "hooks": { diff --git a/src/Map.svg b/src/assets/Map.svg similarity index 100% rename from src/Map.svg rename to src/assets/Map.svg diff --git a/src/favicon.svg b/src/assets/favicon.svg similarity index 100% rename from src/favicon.svg rename to src/assets/favicon.svg diff --git a/src/assets/i18n/locales/en/translation.json b/src/assets/i18n/locales/en/translation.json new file mode 100644 index 0000000..bded201 --- /dev/null +++ b/src/assets/i18n/locales/en/translation.json @@ -0,0 +1,7 @@ +{ + "title": "Welcome to react using react-i18next", + "description": { + "part1": "To get started, edit <1>src/App.js</1> and save to reload.", + "part2": "Switch language between english and german using buttons above." + } +} diff --git a/src/assets/i18n/locales/fr/translation.json b/src/assets/i18n/locales/fr/translation.json new file mode 100644 index 0000000..784b45d --- /dev/null +++ b/src/assets/i18n/locales/fr/translation.json @@ -0,0 +1,7 @@ +{ + "title": "Bienvenue react-i18next", + "description": { + "part1": "To get started, edit <1>src/App.js</1> and save to reload.", + "part2": "Switch language between english and german using buttons above." + } +} diff --git a/src/logo.svg b/src/assets/logo.svg similarity index 100% rename from src/logo.svg rename to src/assets/logo.svg diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index f1e74d0..cb58973 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -9,7 +9,7 @@ import { } from '@elastic/eui'; import HeaderUserMenu from './header_user_menu'; import style from './styles'; -import logoInSylva from '../../favicon.svg'; +import logoInSylva from '../../assets/favicon.svg'; const structure = [ { diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..9353fa6 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import en from './assets/i18n/locales/en/translation.json'; +import fr from './assets/i18n/locales/fr/translation.json'; + +const resources = { + en: { + translation: en, + }, + fr: { + translation: fr, + }, +}; + +i18n.use(initReactI18next).init({ + lng: 'fr', + fallbackLng: 'fr', + resources, + debug: true, + interpolation: { + // not needed for react as it escapes by default + escapeValue: false, + }, +}); + +export default i18n; diff --git a/src/index.js b/src/index.js index ab31dbf..68ea85b 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ 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'; const userId = getUrlParam('kcId', ''); const accessToken = getUrlParam('accessToken', ''); diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js index c54f908..fe80cd6 100644 --- a/src/pages/home/Home.js +++ b/src/pages/home/Home.js @@ -6,8 +6,11 @@ import { EuiPageContentBody, EuiTitle, } from '@elastic/eui'; +import { useTranslation } from 'react-i18next'; const Home = () => { + const { t } = useTranslation(); + return ( <> <EuiPageContent> @@ -16,6 +19,7 @@ const Home = () => { <EuiTitle> <h2>Welcome to the IN-SYLVA IS application search module</h2> </EuiTitle> + <p>{t('title')}</p> <br /> <br /> <p> -- GitLab From ffdd89de611a0cfd38f859c04c9cefb2d3ad5e19 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Thu, 25 Apr 2024 16:28:17 +0200 Subject: [PATCH 02/17] corrected index.html --- public/index.html | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/public/index.html b/public/index.html index 38223fe..5e6c564 100644 --- a/public/index.html +++ b/public/index.html @@ -1,17 +1,11 @@ <!DOCTYPE html> <html lang="en"> - <head> <meta charset="utf-8" /> - <!-- <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> --> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> - <!-- - manifest.json provides metadata used when your web app is installed on a - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ - --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> - <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. @@ -21,23 +15,11 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>IN-SYLVA SEARCH</title> + <title>IN-SYLVA Search</title> <script src="%PUBLIC_URL%/env-config.js"></script> </head> - <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> - <!-- - This HTML file is a template. - If you open it directly in the browser, you will see an empty page. - - You can add webfonts, meta tags, or analytics to this file. - The build step will place the bundled scripts into the <body> tag. - - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. - --> </body> - </html> -- GitLab From e41f56b0d1dcbb04df1f2dc507d395b37d801043 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 26 Apr 2024 16:55:24 +0200 Subject: [PATCH 03/17] multi translation files setup with lazy loading --- package.json | 7 +- public/locales/en/common.json | 1 + public/locales/en/header.json | 4 + public/locales/en/home.json | 3 + public/locales/en/maps.json | 7 + public/locales/en/profile.json | 3 + public/locales/en/results.json | 5 + public/locales/en/search.json | 3 + public/locales/en/validation.json | 1 + public/locales/fr/common.json | 1 + public/locales/fr/header.json | 4 + public/locales/fr/home.json | 7 + public/locales/fr/maps.json | 7 + public/locales/fr/profile.json | 3 + public/locales/fr/results.json | 5 + public/locales/fr/search.json | 3 + public/locales/fr/validation.json | 1 + src/assets/i18n/locales/en/translation.json | 7 - src/assets/i18n/locales/fr/translation.json | 7 - src/components/Header/Header.js | 9 +- src/components/Loading/Loading.js | 12 + src/components/Loading/package.json | 6 + src/components/Loading/styles.js | 11 + src/i18n.js | 37 +- src/index.js | 7 +- src/pages/home/Home.js | 8 +- src/pages/maps/SearchMap.js | 406 ++++++-------------- src/pages/maps/styles.js | 117 ++++++ src/pages/profile/Profile.js | 61 ++- src/pages/results/Results.js | 113 +++--- src/pages/search/Search.js | 8 +- 31 files changed, 429 insertions(+), 445 deletions(-) create mode 100644 public/locales/en/common.json create mode 100644 public/locales/en/header.json create mode 100644 public/locales/en/home.json create mode 100644 public/locales/en/maps.json create mode 100644 public/locales/en/profile.json create mode 100644 public/locales/en/results.json create mode 100644 public/locales/en/search.json create mode 100644 public/locales/en/validation.json create mode 100644 public/locales/fr/common.json create mode 100644 public/locales/fr/header.json create mode 100644 public/locales/fr/home.json create mode 100644 public/locales/fr/maps.json create mode 100644 public/locales/fr/profile.json create mode 100644 public/locales/fr/results.json create mode 100644 public/locales/fr/search.json create mode 100644 public/locales/fr/validation.json delete mode 100644 src/assets/i18n/locales/en/translation.json delete mode 100644 src/assets/i18n/locales/fr/translation.json create mode 100644 src/components/Loading/Loading.js create mode 100644 src/components/Loading/package.json create mode 100644 src/components/Loading/styles.js create mode 100644 src/pages/maps/styles.js diff --git a/package.json b/package.json index 536d40d..4faa70a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@material-ui/lab": "^4.0.0-alpha.48", "@material-ui/styles": "^4.10.0", "downloadjs": "^1.4.7", + "i18next": "^23.11.2", + "i18next-http-backend": "^2.5.1", "moment": "^2.27.0", "mui-datatables": "^3.4.0", "ol": "^6.3.2-dev.1594217558556", @@ -20,10 +22,9 @@ "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", - "i18next": "^23.11.2", - "react-i18next": "^14.1.1" + "react-scripts": "^3.3.0" }, "scripts": { "start": "react-scripts start", diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1 @@ +{} diff --git a/public/locales/en/header.json b/public/locales/en/header.json new file mode 100644 index 0000000..9342625 --- /dev/null +++ b/public/locales/en/header.json @@ -0,0 +1,4 @@ +{ + "home": "Home", + "search": "Search" +} diff --git a/public/locales/en/home.json b/public/locales/en/home.json new file mode 100644 index 0000000..0ae6026 --- /dev/null +++ b/public/locales/en/home.json @@ -0,0 +1,3 @@ +{ + "pageTitle": "Welcome on In-Sylva search module's homepage." +} diff --git a/public/locales/en/maps.json b/public/locales/en/maps.json new file mode 100644 index 0000000..2e100cb --- /dev/null +++ b/public/locales/en/maps.json @@ -0,0 +1,7 @@ +{ + "layersChoiceTitle": "Click on layers to toggle display.", + "layersTableHeaders": { + "cartography": "Cartography", + "data": "Data" + } +} diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json new file mode 100644 index 0000000..7c610a5 --- /dev/null +++ b/public/locales/en/profile.json @@ -0,0 +1,3 @@ +{ + "pageTitle": "Profile management" +} diff --git a/public/locales/en/results.json b/public/locales/en/results.json new file mode 100644 index 0000000..742539e --- /dev/null +++ b/public/locales/en/results.json @@ -0,0 +1,5 @@ +{ + "downloadResultsButton": { + "JSON": "Download as JSON" + } +} diff --git a/public/locales/en/search.json b/public/locales/en/search.json new file mode 100644 index 0000000..26c286e --- /dev/null +++ b/public/locales/en/search.json @@ -0,0 +1,3 @@ +{ + "pageTitle": "In-Sylva Metadata Search Platform" +} diff --git a/public/locales/en/validation.json b/public/locales/en/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/locales/en/validation.json @@ -0,0 +1 @@ +{} diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/locales/fr/common.json @@ -0,0 +1 @@ +{} diff --git a/public/locales/fr/header.json b/public/locales/fr/header.json new file mode 100644 index 0000000..73052c5 --- /dev/null +++ b/public/locales/fr/header.json @@ -0,0 +1,4 @@ +{ + "home": "Page d'accueil", + "search": "Recherche" +} diff --git a/public/locales/fr/home.json b/public/locales/fr/home.json new file mode 100644 index 0000000..6f2a42e --- /dev/null +++ b/public/locales/fr/home.json @@ -0,0 +1,7 @@ +{ + "pageTitle": "Bienvenue sur la page d'accueil du module de recherche du SI In-Sylva", + "description": { + "part1": "rezer", + "part2": "rezer" + } +} diff --git a/public/locales/fr/maps.json b/public/locales/fr/maps.json new file mode 100644 index 0000000..afe4298 --- /dev/null +++ b/public/locales/fr/maps.json @@ -0,0 +1,7 @@ +{ + "layersChoiceTitle": "Cliquez sur les couches pour modifier l'affichage.", + "layersTableHeaders": { + "cartography": "Cartographie", + "data": "Données" + } +} diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json new file mode 100644 index 0000000..983d813 --- /dev/null +++ b/public/locales/fr/profile.json @@ -0,0 +1,3 @@ +{ + "pageTitle": "Gestion du profil" +} diff --git a/public/locales/fr/results.json b/public/locales/fr/results.json new file mode 100644 index 0000000..498fc27 --- /dev/null +++ b/public/locales/fr/results.json @@ -0,0 +1,5 @@ +{ + "downloadResultsButton": { + "JSON": "Télécharger en JSON" + } +} diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json new file mode 100644 index 0000000..866c88a --- /dev/null +++ b/public/locales/fr/search.json @@ -0,0 +1,3 @@ +{ + "pageTitle": "Plateforme de recherche de métadonnées In-Sylva" +} diff --git a/public/locales/fr/validation.json b/public/locales/fr/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/locales/fr/validation.json @@ -0,0 +1 @@ +{} diff --git a/src/assets/i18n/locales/en/translation.json b/src/assets/i18n/locales/en/translation.json deleted file mode 100644 index bded201..0000000 --- a/src/assets/i18n/locales/en/translation.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Welcome to react using react-i18next", - "description": { - "part1": "To get started, edit <1>src/App.js</1> and save to reload.", - "part2": "Switch language between english and german using buttons above." - } -} diff --git a/src/assets/i18n/locales/fr/translation.json b/src/assets/i18n/locales/fr/translation.json deleted file mode 100644 index 784b45d..0000000 --- a/src/assets/i18n/locales/fr/translation.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Bienvenue react-i18next", - "description": { - "part1": "To get started, edit <1>src/App.js</1> and save to reload.", - "part2": "Switch language between english and german using buttons above." - } -} diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index cb58973..376a143 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -10,23 +10,26 @@ import { import HeaderUserMenu from './header_user_menu'; import style from './styles'; import logoInSylva from '../../assets/favicon.svg'; +import { useTranslation } from 'react-i18next'; const structure = [ { id: 0, - label: 'Home', + label: 'home', href: '/app/home', icon: '', }, { id: 1, - label: 'Search', + label: 'search', href: '/app/search', icon: '', }, ]; const Header = () => { + const { t } = useTranslation('header'); + return ( <> <EuiHeader> @@ -43,7 +46,7 @@ const Header = () => { <EuiHeaderLinks border="right"> {structure.map((link) => ( <EuiHeaderLink iconType="empty" key={link.id}> - <Link to={link.href}>{link.label}</Link> + <Link to={link.href}>{t(link.label)}</Link> </EuiHeaderLink> ))} </EuiHeaderLinks> diff --git a/src/components/Loading/Loading.js b/src/components/Loading/Loading.js new file mode 100644 index 0000000..f5213a7 --- /dev/null +++ b/src/components/Loading/Loading.js @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './styles'; + +const Loading = () => { + return ( + <div style={styles.container}> + <h1>Loading...</h1> + </div> + ); +}; + +export default Loading; diff --git a/src/components/Loading/package.json b/src/components/Loading/package.json new file mode 100644 index 0000000..b4c7045 --- /dev/null +++ b/src/components/Loading/package.json @@ -0,0 +1,6 @@ +{ + "name": "Loading", + "version": "1.0.0", + "private": true, + "main": "Loading.js" +} diff --git a/src/components/Loading/styles.js b/src/components/Loading/styles.js new file mode 100644 index 0000000..6c4d270 --- /dev/null +++ b/src/components/Loading/styles.js @@ -0,0 +1,11 @@ +const styles = { + container: { + width: '100vw', + height: '100vh', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, +}; + +export default styles; diff --git a/src/i18n.js b/src/i18n.js index 9353fa6..65a0362 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,26 +1,21 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -import en from './assets/i18n/locales/en/translation.json'; -import fr from './assets/i18n/locales/fr/translation.json'; +import Backend from 'i18next-http-backend'; -const resources = { - en: { - translation: en, - }, - fr: { - translation: fr, - }, -}; - -i18n.use(initReactI18next).init({ - lng: 'fr', - fallbackLng: 'fr', - resources, - debug: true, - interpolation: { - // not needed for react as it escapes by default - escapeValue: false, - }, -}); +i18n + .use(Backend) + .use(initReactI18next) + .init({ + lng: 'fr', + fallbackLng: 'fr', + defaultNS: 'home', + debug: true, + load: 'languageOnly', + loadPath: 'locales/{{lng}}/{{ns}}.json', + interpolation: { + // not needed for react as it escapes by default + escapeValue: false, + }, + }); export default i18n; diff --git a/src/index.js b/src/index.js index 68ea85b..285bd62 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,11 @@ -import React from 'react'; +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'; const userId = getUrlParam('kcId', ''); const accessToken = getUrlParam('accessToken', ''); @@ -17,7 +18,9 @@ checkUserLogin(userId, accessToken, refreshToken); if (sessionStorage.getItem('access_token')) { ReactDOM.render( <UserProvider> - <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> + <Suspense fallback={<Loading />}> + <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> + </Suspense> </UserProvider>, document.getElementById('root') ); diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js index fe80cd6..9eb375b 100644 --- a/src/pages/home/Home.js +++ b/src/pages/home/Home.js @@ -9,7 +9,7 @@ import { import { useTranslation } from 'react-i18next'; const Home = () => { - const { t } = useTranslation(); + const { t } = useTranslation('home'); return ( <> @@ -17,10 +17,8 @@ const Home = () => { <EuiPageContentHeader> <EuiPageContentHeaderSection> <EuiTitle> - <h2>Welcome to the IN-SYLVA IS application search module</h2> + <h2>{t('pageTitle')}</h2> </EuiTitle> - <p>{t('title')}</p> - <br /> <br /> <p> As a reminder, it should be remembered that the metadata stored in IN-SYLVA @@ -32,7 +30,6 @@ const Home = () => { made up of a series of fields accompanied by their value. </p> <br /> - <br /> <p> With this part of the interface you will be able to search for metadata records (previously loaded via the portal), by defining a certain number of @@ -50,7 +47,6 @@ const Home = () => { via which you can do more precise searches on one or more targeted fields. </p> <br /> - <br /> <p>Click on the "Search" tab to access the search interface.</p> </EuiPageContentHeaderSection> </EuiPageContentHeader> diff --git a/src/pages/maps/SearchMap.js b/src/pages/maps/SearchMap.js index 958af49..64a2beb 100644 --- a/src/pages/maps/SearchMap.js +++ b/src/pages/maps/SearchMap.js @@ -1,18 +1,19 @@ import React, { useState, useEffect } from 'react'; import { Map, View } from 'ol'; -// import TileLayer from "ol/layer/Tile"; import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'; +//import {fromLonLat, get as getProjection} from 'ol/proj.js'; +// import TileLayer from 'ol/layer/Tile'; import ImageLayer from 'ol/layer/Image'; +import GeoJSON from 'ol/format/GeoJSON'; +import ImageWMS from 'ol/source/ImageWMS'; +//import { Circle as CircleStyle, Fill, Stroke, Style, Text, Icon } from 'ol/style'; +import { toStringXY } from 'ol/coordinate'; import SourceOSM from 'ol/source/OSM'; import BingMaps from 'ol/source/BingMaps'; import { Vector as VectorSource } from 'ol/source'; import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; -//import {fromLonLat, get as getProjection} from 'ol/proj.js'; import { getWidth } from 'ol/extent'; -import ImageWMS from 'ol/source/ImageWMS'; -import GeoJSON from 'ol/format/GeoJSON'; -//import { Circle as CircleStyle, Fill, Stroke, Style, Text, Icon } from 'ol/style'; import { Fill, Stroke, Style, Text, Icon } from 'ol/style'; import { Circle, Point, Polygon } from 'ol/geom'; import Feature from 'ol/Feature'; @@ -23,192 +24,43 @@ import { OverviewMap, defaults as defaultControls, } from 'ol/control'; -import { toStringXY } from 'ol/coordinate'; import 'ol/ol.css'; import { EuiCheckbox } from '@elastic/eui'; import { htmlIdGenerator } from '@elastic/eui/lib/services'; import { updateArrayElement } from '../../Utils.js'; +import { useTranslation } from 'react-i18next'; +import styles from './styles'; -const SearchMap = (props) => { - /*var image = new CircleStyle({ - radius: 5, - fill: null, - stroke: new Stroke({ color: 'red', width: 1 }), - });*/ - const styles = { - Point: new Style({ - image: new Icon({ - anchor: [0.5, 46], - anchorXUnits: 'fraction', - anchorYUnits: 'pixels', - src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png', - }), - }), - /* 'Circle': new Style({ - image: new Circle({ - radius: 7, - fill: new Fill({ - color: 'green' - }), - stroke: new Stroke({ - color: 'blue', - width: 2 - }) - }) - }),*/ - Circle: new Style({ - stroke: new Stroke({ - color: 'blue', - width: 2, - }), - //radius: 1000, - fill: new Fill({ - color: 'rgba(0,0,255,0.3)', - }), - }), - /* 'LineString': new Style({ - stroke: new Stroke({ - color: 'green', - width: 1, - }), - }), - 'MultiLineString': new Style({ - stroke: new Stroke({ - color: 'green', - width: 1, - }), - }), - 'MultiPoint': new Style({ - image: image, - }), - 'MultiPolygon': new Style({ - stroke: new Stroke({ - color: 'yellow', - width: 1, - }), - fill: new Fill({ - color: 'rgba(255, 255, 0, 0.1)', - }), - }), - 'Polygon': new Style({ - stroke: new Stroke({ - color: 'blue', - lineDash: [4], - width: 3, - }), - fill: new Fill({ - color: 'rgba(0, 0, 255, 0.1)', - }), - }), - 'GeometryCollection': new Style({ - stroke: new Stroke({ - color: 'magenta', - width: 2, - }), - fill: new Fill({ - color: 'magenta', - }), - image: new CircleStyle({ - radius: 10, - fill: null, - stroke: new Stroke({ - color: 'magenta', - }), - }), - }), - 'Circle': new Style({ - stroke: new Stroke({ - color: 'red', - width: 2, - }), - fill: new Fill({ - color: 'rgba(255,0,0,0.2)', - }), - }), - bluecircle: { - width: 30, - height: 30, - border: "1px solid #088", - bordeRadius: "15", - backgroundColor: "#0ff", - opacity: 0.5, - zIndex: 9999 - }, */ - mapContainer: { - height: '80vh', - width: '60vw', - }, - layerTree: { - cursor: 'pointer', - }, - }; - const source = new SourceOSM(); - const overviewMapControl = new OverviewMap({ - layers: [ - new TileLayer({ - source: source, - }), - ], - }); - const [center, setCenter] = useState(proj.fromLonLat([2.5, 46.5])); - const [zoom, setZoom] = useState(6); - const styleFunction = function (feature) { - return styles[feature.getGeometry().getType()]; - }; - - /* const newPoint = { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': proj.fromLonLat([6.37777777778, 43.1938888889]), - }, - 'properties': { - 'Site': 'TEST' - } - } - - const geojsonObject = { - 'type': 'FeatureCollection', - 'crs': { - 'type': 'name', - 'properties': { - 'name': 'EPSG:4326', - }, - }, - 'features': [ - // newPoint - ], - } */ - - const vectorSource = new VectorSource({ - /*features: new GeoJSON().readFeatures(geojsonObject),*/ - projection: 'EPSG:4326', - }); - - // vectorSource.addFeature(new Feature(new Polygon([[proj.fromLonLat([0, 45]), proj.fromLonLat([0, 50]), proj.fromLonLat([5, 50]), proj.fromLonLat([5, 45])]]))); - - const vectorLayer = new VectorLayer({ - name: 'query_results', - source: vectorSource, - style: styleFunction, - }); - +const initResolutions = () => { const resolutions = []; - const matrixIds = []; const proj3857 = proj.get('EPSG:3857'); - const maxResolution = getWidth(proj3857.getExtent()) / 256; + const maxResolution = getWidth(proj3857.getExtent()) / 256; for (let i = 0; i < 20; i++) { - matrixIds[i] = i.toString(); resolutions[i] = maxResolution / Math.pow(2, i); } + return resolutions; +}; - const tileGrid = new WMTSTileGrid({ - origin: [-20037508, 20037508], - resolutions: resolutions, - matrixIds: matrixIds, - }); +const initMatrixIds = () => { + const matrixIds = []; + for (let i = 0; i < 20; i++) { + matrixIds[i] = i.toString(); + } + return matrixIds; +}; + +const SearchMap = (props) => { + const { t } = useTranslation('maps'); + const source = new SourceOSM(); + const [center, setCenter] = useState(proj.fromLonLat([2.5, 46.5])); + const [zoom, setZoom] = useState(6); + const [vectorSource, setVectorSource] = useState( + new VectorSource({ + projection: 'EPSG:4326', + }) + ); const [mapLayers, setMapLayers] = useState([ new TileLayer({ name: 'osm-layer', @@ -232,7 +84,11 @@ const SearchMap = (props) => { matrixSet: 'PM', format: 'image/png', projection: 'EPSG:3857', - tileGrid: tileGrid, + tileGrid: new WMTSTileGrid({ + origin: [-20037508, 20037508], + resolutions: initResolutions(), + matrixIds: initMatrixIds(), + }), style: 'normal', attributions: '<a href="https://www.ign.fr/" target="_blank">' + @@ -240,29 +96,14 @@ const SearchMap = (props) => { 'information géographique et forestière" alt="IGN"></a>', }), }), - vectorLayer, - - // new ImageLayer({ - // name: 'donuts-insylva-layer', - // source: new ImageWMS({ - // url: 'http: //w3.avignon.inra.fr/geoserver/wms', - // params: { 'LAYERS': 'urfm:donut_view' }, - // ratio: 1 - // }) - // }) + new VectorLayer({ + name: 'query_results', + source: vectorSource, + style: (feature) => { + return styles[feature.getGeometry().getType()]; + }, + }), ]); - - const [mapLayersVisibility, setMapLayersVisibility] = useState( - new Array(mapLayers.length).fill(true) - ); - - // const posGreenwich = proj.fromLonLat([0, 51.47]); - // set initial map objects - const view = new View({ - center: center, - zoom: zoom, - }); - const [map] = useState( new Map({ target: null, @@ -272,35 +113,24 @@ const SearchMap = (props) => { projection: 'EPSG:4326', }), new ScaleLine(), - overviewMapControl, + new OverviewMap({ + layers: [ + new TileLayer({ + source: source, + }), + ], + }), ]), - view: view, + view: new View({ + center: center, + zoom: zoom, + }), }) ); + const [mapLayersVisibility, setMapLayersVisibility] = useState( + new Array(mapLayers.length).fill(true) + ); - const processData = (props) => { - if (props.searchResults) { - props.searchResults.forEach((result) => { - if ( - result.experimental_site.geo_point && - result.experimental_site.geo_point.longitude && - result.experimental_site.geo_point.latitude - ) { - //vectorSource.addFeature(new Feature(new Point(proj.fromLonLat([result.experimental_site.geo_point.longitude, result.experimental_site.geo_point.latitude])))) - //vectorSource.addFeature(new Feature(new Circle(toStringXY([result.experimental_site.geo_point.longitude, result.experimental_site.geo_point.latitude],1),10))) - const coord = [ - result.experimental_site.geo_point.longitude, - result.experimental_site.geo_point.latitude, - ]; - vectorSource.addFeature(new Feature(new Circle(proj.fromLonLat(coord), 1000))); - //vectorSource.addFeature(new Feature(new Circle([result.experimental_site.geo_point.longitude, result.experimental_site.geo_point.latitude],10))) - } - }); - } - }; - - // useEffect Hooks - // [] = component did mount // set the initial map targets useEffect(() => { map.setTarget('map'); @@ -308,7 +138,6 @@ const SearchMap = (props) => { setCenter(map.getView().getCenter()); setZoom(map.getView().getZoom()); }); - /* map.getCurrentScale = function () { //var map = this.getMap(); var map = this; @@ -319,7 +148,6 @@ const SearchMap = (props) => { var mpu = proj.METERS_PER_UNIT[units]; var scale = resolution * mpu * 39.37 * dpi; return scale; - }; map.getView().on('change:resolution', function(evt){ @@ -337,14 +165,31 @@ const SearchMap = (props) => { }); map.addOverlay(overlay); */ map.getView().animate({ zoom: zoom }, { center: center }, { duration: 2000 }); - processData(props); - // clean up upon component unmount - /* return () => { - map.setTarget(null); - }; */ }, [props]); + // vectorSource.addFeature(new Feature(new Polygon([[proj.fromLonLat([0, 45]), proj.fromLonLat([0, 50]), proj.fromLonLat([5, 50]), proj.fromLonLat([5, 45])]]))); + + const processData = (props) => { + if (props.searchResults) { + props.searchResults.forEach((result) => { + if ( + result.experimental_site.geo_point && + result.experimental_site.geo_point.longitude && + result.experimental_site.geo_point.latitude + ) { + //vectorSource.addFeature(new Feature(new Point(proj.fromLonLat([result.experimental_site.geo_point.longitude, result.experimental_site.geo_point.latitude])))) + //vectorSource.addFeature(new Feature(new Circle(toStringXY([result.experimental_site.geo_point.longitude, result.experimental_site.geo_point.latitude],1),10))) + const pos = [ + result.experimental_site.geo_point.longitude, + result.experimental_site.geo_point.latitude, + ]; + vectorSource.addFeature(new Feature(new Circle(proj.fromLonLat(pos), 1000))); + } + }); + } + }; + const getLayerIndex = (name) => { let index = 0; mapLayers.forEach((layer) => { @@ -358,7 +203,6 @@ const SearchMap = (props) => { const toggleLayer = (name) => { let updatedLayers = mapLayers; const layerIndex = getLayerIndex(name); - // let updatedLayer = updatedLayers[getLayerIndex(name)] setMapLayersVisibility( updateArrayElement( mapLayersVisibility, @@ -370,41 +214,21 @@ const SearchMap = (props) => { setMapLayers(updatedLayers); }; - // helpers - /* const btnAction = () => { - // when button is clicked, recentre map - // this does not work :( - setCenter(posGreenwich); - setZoom(6); - }; */ - // render return ( - <div> + <> <div id="map" style={styles.mapContainer}></div> <div id="layertree"> - <br /> - <h5>Cliquez sur les couches pour modifier leur visibilité.</h5> - <br /> - <table> + <table style={styles.legendTable}> <thead> <tr> - <th>Fonds carto ou vecteur</th> - {/*<th align="center">Couches INSYLVA</th> - <th>Infos attributs</th>*/} + <th>{t('layersTableHeaders.cartography')}</th> + <th>{t('layersTableHeaders.data')}</th> </tr> </thead> <tbody> <tr> - <td> + <td style={styles.legendTableCells}> <ul> - <li> - <EuiCheckbox - id={htmlIdGenerator()()} - label="Query result" - checked={mapLayersVisibility[getLayerIndex('query_results')]} - onChange={(e) => toggleLayer('query_results')} - /> - </li> <li> <EuiCheckbox id={htmlIdGenerator()()} @@ -429,51 +253,39 @@ const SearchMap = (props) => { onChange={(e) => toggleLayer('IGN')} /> </li> - {/*<li> - <EuiCheckbox - id={htmlIdGenerator()()} - label="Départements" - checked={mapLayersVisibility[getLayerIndex("dept-layer")]} - onChange={e => toggleLayer("dept-layer")} - /> - </li> - <li> - <EuiCheckbox - id={htmlIdGenerator()()} - label="Régions" - checked={mapLayersVisibility[getLayerIndex("regs-layer")]} - onChange={e => toggleLayer("regs-layer")} - /> - </li>*/} + {/* + <li> + <EuiCheckbox + id={htmlIdGenerator()()} + label="Départements" + checked={mapLayersVisibility[getLayerIndex("dept-layer")]} + onChange={e => toggleLayer("dept-layer")} + /> + </li> + <li> + <EuiCheckbox + id={htmlIdGenerator()()} + label="Régions" + checked={mapLayersVisibility[getLayerIndex("regs-layer")]} + onChange={e => toggleLayer("regs-layer")} + /> + </li> + */} </ul> </td> - <td> - <div id="info"> </div> + <td style={styles.legendTableCells}> + <EuiCheckbox + id={htmlIdGenerator()()} + label="Query result" + checked={mapLayersVisibility[getLayerIndex('query_results')]} + onChange={(e) => toggleLayer('query_results')} + /> </td> </tr> </tbody> </table> </div> - {/*<div - style={styles.bluecircle} - ref={overlayRef} - id="overlay" - title="overlay" - />*/} - {/*<button - style={{ - position: "absolute", - right: 10, - top: 10, - backgroundColor: "white" - }} - onClick={() => { - btnAction(); - }} - > - CLICK - </button>*/} - </div> + </> ); }; diff --git a/src/pages/maps/styles.js b/src/pages/maps/styles.js new file mode 100644 index 0000000..a991862 --- /dev/null +++ b/src/pages/maps/styles.js @@ -0,0 +1,117 @@ +import { Fill, Icon, Stroke, Style } from 'ol/style'; + +const styles = { + Point: new Style({ + image: new Icon({ + anchor: [0.5, 46], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png', + }), + }), + /* 'Circle': new Style({ + image: new Circle({ + radius: 7, + fill: new Fill({ + color: 'green' + }), + stroke: new Stroke({ + color: 'blue', + width: 2 + }) + }) + }),*/ + Circle: new Style({ + stroke: new Stroke({ + color: 'blue', + width: 2, + }), + //radius: 1000, + fill: new Fill({ + color: 'rgba(0,0,255,0.3)', + }), + }), + /* 'LineString': new Style({ + stroke: new Stroke({ + color: 'green', + width: 1, + }), + }), + 'MultiLineString': new Style({ + stroke: new Stroke({ + color: 'green', + width: 1, + }), + }), + 'MultiPoint': new Style({ + image: image, + }), + 'MultiPolygon': new Style({ + stroke: new Stroke({ + color: 'yellow', + width: 1, + }), + fill: new Fill({ + color: 'rgba(255, 255, 0, 0.1)', + }), + }), + 'Polygon': new Style({ + stroke: new Stroke({ + color: 'blue', + lineDash: [4], + width: 3, + }), + fill: new Fill({ + color: 'rgba(0, 0, 255, 0.1)', + }), + }), + 'GeometryCollection': new Style({ + stroke: new Stroke({ + color: 'magenta', + width: 2, + }), + fill: new Fill({ + color: 'magenta', + }), + image: new CircleStyle({ + radius: 10, + fill: null, + stroke: new Stroke({ + color: 'magenta', + }), + }), + }), + 'Circle': new Style({ + stroke: new Stroke({ + color: 'red', + width: 2, + }), + fill: new Fill({ + color: 'rgba(255,0,0,0.2)', + }), + }), + bluecircle: { + width: 30, + height: 30, + border: "1px solid #088", + bordeRadius: "15", + backgroundColor: "#0ff", + opacity: 0.5, + zIndex: 9999 + }, */ + mapContainer: { + height: '80vh', + width: '60vw', + }, + layerTree: { + cursor: 'pointer', + }, + legendTable: { + margin: '20px', + }, + legendTableCells: { + padding: '10px', + }, +}; + +export default styles; diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js index d6fecd3..3e8c303 100644 --- a/src/pages/profile/Profile.js +++ b/src/pages/profile/Profile.js @@ -23,20 +23,10 @@ import { createUserRequest, deleteUserRequest, } from '../../actions/user'; - -/* const fieldsGridOptions = { - filter: true, - filterType: "dropdown", - responsive: "stacked", - selectableRows: 'multiple', - selectableRowsOnClick: true, - onRowsSelect: (rowsSelected, allRows) => { - }, - onRowClick: (rowData, rowState) => { - }, -}; */ +import { useTranslation } from 'react-i18next'; const Profile = () => { + const { t } = useTranslation('profile'); const [user, setUser] = useState({}); const [userRole, setUserRole] = useState(''); const [groups, setGroups] = useState([]); @@ -47,11 +37,32 @@ const Profile = () => { const [valueError, setValueError] = useState(undefined); useEffect(() => { + const loadUser = () => { + if (sessionStorage.getItem('user_id')) { + findOneUser(sessionStorage.getItem('user_id')).then((user) => { + setUser(user); + }); + findOneUserWithGroupAndRole(sessionStorage.getItem('user_id')).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: 'Group Name', width: '30%' }, @@ -112,28 +123,6 @@ const Profile = () => { { name: 'Delete', actions: requestActions }, ]; - const loadUser = () => { - if (sessionStorage.getItem('user_id')) { - findOneUser(sessionStorage.getItem('user_id')).then((user) => { - setUser(user); - }); - findOneUserWithGroupAndRole(sessionStorage.getItem('user_id')).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); - }); - } - }; - const getUserGroupLabels = () => { let labelList = ''; if (!!userGroups) { @@ -161,7 +150,7 @@ const Profile = () => { <EuiPageContentHeader> <EuiPageContentHeaderSection> <EuiTitle> - <h2>Profile management</h2> + <h2>{t('pageTitle')}</h2> </EuiTitle> </EuiPageContentHeaderSection> </EuiPageContentHeader> diff --git a/src/pages/results/Results.js b/src/pages/results/Results.js index c54481e..0c61fce 100644 --- a/src/pages/results/Results.js +++ b/src/pages/results/Results.js @@ -15,8 +15,8 @@ import { createTheme, MuiThemeProvider } from '@material-ui/core/styles'; import MUIDataTable from 'mui-datatables'; import JsonView from '@in-sylva/json-view'; import { updateArrayElement } from '../../Utils.js'; - -const download = require('downloadjs'); +import download from 'downloadjs'; +import { useTranslation } from 'react-i18next'; const getMuiTheme = () => createTheme({ @@ -39,6 +39,7 @@ const changeFlyoutState = (array, index, value, defaultValue) => { }; const Results = (searchResults, search, basicSearch) => { + const { t } = useTranslation('results'); const [resultsCol, setResultsCol] = useState([]); const [results, setResults] = useState([]); const [isFlyoutOpen, setIsFlyoutOpen] = useState([false]); @@ -62,6 +63,48 @@ const Results = (searchResults, search, basicSearch) => { return updatedResults; }; + const processData = (metadata) => { + if (metadata) { + const columns = []; + const rows = []; + // const metadataRecords = metadata.hits.hits + columns.push({ + name: 'currently open', + options: { + display: true, + viewColumns: true, + filter: true, + }, + }); + for (let recordIndex = 0; recordIndex < metadata.length; recordIndex++) { + const row = []; + const displayedFields = metadata[recordIndex].resource; + const flyoutCell = recordFlyout(metadata[recordIndex], recordIndex); + if (recordIndex >= isFlyoutOpen.length) { + setIsFlyoutOpen([...isFlyoutOpen, false]); + } + row.push(flyoutCell); + for (const fieldName in displayedFields) { + if (typeof displayedFields[fieldName] === 'string') { + if (recordIndex === 0) { + const column = { + name: fieldName, + options: { + display: true, + }, + }; + columns.push(column); + } + row.push(displayedFields[fieldName]); + } + } + rows.push(row); + } + setResultsCol(columns); + setResults(rows); + } + }; + const recordFlyout = (record, recordIndex, isOpen) => { if (isOpen) { return ( @@ -270,50 +313,13 @@ const Results = (searchResults, search, basicSearch) => { ) } */ - const processData = (metadata) => { - // if (metadata && metadata.hits) { - if (metadata) { - const columns = []; - const rows = []; - // const metadataRecords = metadata.hits.hits - columns.push({ - name: 'currently open', - options: { - display: true, - viewColumns: true, - filter: true, - }, - }); - /* for (let recordIndex = 0; recordIndex < metadataRecords.length; recordIndex++) { - const row = [] - const displayedFields = metadataRecords[recordIndex]._source.resource - const flyoutCell = recordFlyout(metadataRecords[recordIndex]._source, recordIndex) */ - for (let recordIndex = 0; recordIndex < metadata.length; recordIndex++) { - const row = []; - const displayedFields = metadata[recordIndex].resource; - const flyoutCell = recordFlyout(metadata[recordIndex], recordIndex); - if (recordIndex >= isFlyoutOpen.length) { - setIsFlyoutOpen([...isFlyoutOpen, false]); - } - row.push(flyoutCell); - for (const fieldName in displayedFields) { - if (typeof displayedFields[fieldName] === 'string') { - if (recordIndex === 0) { - const column = { - name: fieldName, - options: { - display: true, - }, - }; - columns.push(column); - } - row.push(displayedFields[fieldName]); - } - } - rows.push(row); - } - setResultsCol(columns); - setResults(rows); + const downloadResults = () => { + if (searchResults) { + download( + `{"metadataRecords": ${JSON.stringify(searchResults, null, '\t')}}`, + 'InSylvaSearchResults.json', + 'application/json' + ); } }; @@ -338,19 +344,8 @@ const Results = (searchResults, search, basicSearch) => { /> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiButton - fill - onClick={() => { - if (searchResults) { - download( - `{"metadataRecords": ${JSON.stringify(searchResults, null, '\t')}}`, - 'InSylvaSearchResults.json', - 'application/json' - ); - } - }} - > - Download as JSON + <EuiButton fill onClick={() => downloadResults()}> + {t('downloadResultsButton.JSON')} </EuiButton> </EuiFlexItem> </EuiFlexGroup> diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index a1c0044..901e335 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -62,6 +62,7 @@ import { getQueryCount, } from '../../actions/source'; import { addUserHistory, fetchUserHistory } from '../../actions/user'; +import { useTranslation } from 'react-i18next'; const useStyles = makeStyles((theme) => ({ container: { @@ -1516,6 +1517,8 @@ const SourceSelect = ( }; const Search = () => { + const { t } = useTranslation('search'); + const datePickerStyles = useStyles(); const [isLoading, setIsLoading] = useState(false); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [userHistory, setUserHistory] = useState({}); @@ -1545,7 +1548,6 @@ const Search = () => { const [selectedSavedSearch, setSelectedSavedSearch] = useState(); const [historySelectError, setHistorySelectError] = useState(undefined); const [notificationToasts, setNotificationToasts] = useState([]); - const datePickerStyles = useStyles(); useEffect(() => { fetchPublicFields().then((resultStdFields) => { @@ -1842,12 +1844,10 @@ const Search = () => { return ( <> <EuiPageContent> - {' '} - {/*style={{ backgroundColor: "#fafafa" }}*/} <EuiPageContentHeader> <EuiPageContentHeaderSection> <EuiTitle> - <h2>In-Sylva Metadata Search Platform</h2> + <h2>{t('pageTitle')}</h2> </EuiTitle> </EuiPageContentHeaderSection> </EuiPageContentHeader> -- GitLab From 4c944de119a1d901cc385dc938177383e714c1e7 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Mon, 29 Apr 2024 11:16:40 +0200 Subject: [PATCH 04/17] added a component to switch language --- public/locales/en/common.json | 7 +- public/locales/fr/common.json | 7 +- src/components/Header/Header.js | 16 ++-- src/components/Header/HeaderUserMenu.js | 88 +++++++++++++++++++ src/components/Header/header_user_menu.js | 86 ------------------ src/components/Header/styles.js | 16 +++- .../LanguageSwitcher/LanguageSwitcher.js | 29 ++++++ src/components/LanguageSwitcher/styles.js | 7 ++ 8 files changed, 159 insertions(+), 97 deletions(-) create mode 100644 src/components/Header/HeaderUserMenu.js delete mode 100644 src/components/Header/header_user_menu.js create mode 100644 src/components/LanguageSwitcher/LanguageSwitcher.js create mode 100644 src/components/LanguageSwitcher/styles.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 0967ef4..66bbf9e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1 +1,6 @@ -{} +{ + "languages": { + "en": "English", + "fr": "French" + } +} diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 0967ef4..74f9cd8 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -1 +1,6 @@ -{} +{ + "languages": { + "en": "Anglais", + "fr": "Français" + } +} diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 376a143..ed7ce1f 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -7,10 +7,11 @@ import { EuiHeaderLinks, EuiHeaderLink, } from '@elastic/eui'; -import HeaderUserMenu from './header_user_menu'; +import HeaderUserMenu from './HeaderUserMenu'; import style from './styles'; import logoInSylva from '../../assets/favicon.svg'; import { useTranslation } from 'react-i18next'; +import LanguageSwitcher from '../LanguageSwitcher/LanguageSwitcher'; const structure = [ { @@ -34,16 +35,16 @@ const Header = () => { <> <EuiHeader> <EuiHeaderSection grow={true}> - <EuiHeaderSectionItem border="right"> + <EuiHeaderSectionItem> <img - style={style} + style={style.logo} src={logoInSylva} width="75" height="45" alt="Logo INRAE" /> </EuiHeaderSectionItem> - <EuiHeaderLinks border="right"> + <EuiHeaderLinks> {structure.map((link) => ( <EuiHeaderLink iconType="empty" key={link.id}> <Link to={link.href}>{t(link.label)}</Link> @@ -52,7 +53,12 @@ const Header = () => { </EuiHeaderLinks> </EuiHeaderSection> <EuiHeaderSection side="right"> - <EuiHeaderSectionItem>{HeaderUserMenu()}</EuiHeaderSectionItem> + <EuiHeaderSectionItem style={style.languageSwitcherItem} border={'none'}> + <LanguageSwitcher /> + </EuiHeaderSectionItem> + <EuiHeaderSectionItem style={style.userMenuItem} border={'none'}> + <HeaderUserMenu /> + </EuiHeaderSectionItem> </EuiHeaderSection> </EuiHeader> </> diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js new file mode 100644 index 0000000..122e7a2 --- /dev/null +++ b/src/components/Header/HeaderUserMenu.js @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import { + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiText, + EuiSpacer, + EuiPopover, + EuiButtonIcon, +} from '@elastic/eui'; +import { signOut } from '../../context/UserContext'; +import { findOneUser } from '../../actions/user'; + +const HeaderUserMenu = () => { + const [isOpen, setIsOpen] = useState(false); + const [user, setUser] = useState({}); + + const onMenuButtonClick = () => { + setIsOpen(!isOpen); + }; + + const closeMenu = () => { + setIsOpen(false); + }; + + useEffect(() => { + const loadUser = () => { + if (sessionStorage.getItem('user_id')) { + findOneUser(sessionStorage.getItem('user_id')).then((user) => { + setUser(user); + }); + } + }; + + loadUser(); + }, []); + + const HeaderUserButton = ( + <EuiButtonIcon + size="s" + onClick={onMenuButtonClick} + iconType="user" + title="User profile" + aria-label="User profile" + /> + ); + + return user.username ? ( + <EuiPopover + id="headerUserMenu" + ownFocus + button={HeaderUserButton} + isOpen={isOpen} + anchorPosition="downRight" + closePopover={closeMenu} + panelPaddingSize="none" + > + <div style={{ width: 320 }}> + <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">Edit profile</EuiLink> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiLink onClick={() => signOut()}>Log out</EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </div> + </EuiPopover> + ) : ( + <></> + ); +}; + +export default HeaderUserMenu; diff --git a/src/components/Header/header_user_menu.js b/src/components/Header/header_user_menu.js deleted file mode 100644 index feb51bb..0000000 --- a/src/components/Header/header_user_menu.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - EuiAvatar, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, - EuiSpacer, - EuiPopover, - EuiButtonIcon, -} from '@elastic/eui'; -import { signOut } from '../../context/UserContext'; -import { findOneUser } from '../../actions/user'; - -export default function HeaderUserMenu() { - const [isOpen, setIsOpen] = useState(false); - const [user, setUser] = useState({}); - - const onMenuButtonClick = () => { - setIsOpen(!isOpen); - }; - - const closeMenu = () => { - setIsOpen(false); - }; - - const loadUser = () => { - if (sessionStorage.getItem('user_id')) { - findOneUser(sessionStorage.getItem('user_id')).then((user) => { - setUser(user); - }); - } - }; - - useEffect(() => { - loadUser(); - }, []); - - const HeaderUserButton = ( - <EuiButtonIcon - size="s" - onClick={onMenuButtonClick} - iconType="user" - title="User profile" - aria-label="User profile" - /> - ); - - return ( - user.username && ( - <EuiPopover - id="headerUserMenu" - ownFocus - button={HeaderUserButton} - isOpen={isOpen} - anchorPosition="downRight" - closePopover={closeMenu} - panelPaddingSize="none" - > - <div style={{ width: 320 }}> - <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">Edit profile</EuiLink> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiLink onClick={() => signOut()}>Log out</EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </div> - </EuiPopover> - ) - ); -} diff --git a/src/components/Header/styles.js b/src/components/Header/styles.js index 2515727..27b1761 100644 --- a/src/components/Header/styles.js +++ b/src/components/Header/styles.js @@ -1,8 +1,16 @@ const headerStyle = { - paddingTop: '10px', - paddingBottom: '-6px', - paddingRight: '10px', - paddingLeft: '10px', + logo: { + paddingTop: '10px', + paddingBottom: '-6px', + paddingRight: '10px', + paddingLeft: '10px', + }, + languageSwitcherItem: { + margin: '10px', + }, + userMenuItem: { + marginRight: '10px', + }, }; export default headerStyle; diff --git a/src/components/LanguageSwitcher/LanguageSwitcher.js b/src/components/LanguageSwitcher/LanguageSwitcher.js new file mode 100644 index 0000000..7fb068c --- /dev/null +++ b/src/components/LanguageSwitcher/LanguageSwitcher.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './styles'; +import { EuiSelect } from '@elastic/eui'; + +const LanguageSwitcher = () => { + const { t, i18n } = useTranslation('common'); + + const options = [ + { text: t('languages.en'), value: 'en' }, + { text: t('languages.fr'), value: 'fr' }, + ]; + + const changeLanguage = (newLng) => { + i18n.changeLanguage(newLng).then(); + }; + + return ( + <EuiSelect + style={styles.select} + options={options} + compressed={true} + value={i18n.resolvedLanguage} + onChange={(e) => changeLanguage(e.target.value)} + /> + ); +}; + +export default LanguageSwitcher; diff --git a/src/components/LanguageSwitcher/styles.js b/src/components/LanguageSwitcher/styles.js new file mode 100644 index 0000000..9c4c4bb --- /dev/null +++ b/src/components/LanguageSwitcher/styles.js @@ -0,0 +1,7 @@ +const styles = { + select: { + borderRadius: '6px', + }, +}; + +export default styles; -- GitLab From 549ca1b9fea997216c2df800164cd327b08b5711 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Mon, 29 Apr 2024 14:49:48 +0200 Subject: [PATCH 05/17] added full translations for homepage --- public/locales/en/home.json | 10 +++++++++- public/locales/fr/home.json | 12 ++++++++---- src/pages/home/Home.js | 31 ++++++------------------------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/public/locales/en/home.json b/public/locales/en/home.json index 0ae6026..d1038e0 100644 --- a/public/locales/en/home.json +++ b/public/locales/en/home.json @@ -1,3 +1,11 @@ { - "pageTitle": "Welcome on In-Sylva search module's homepage." + "pageTitle": "Welcome on In-Sylva search module's homepage.", + "searchToolDescription": { + "part1": "As a reminder, it should be remembered that the metadata stored in IN-SYLVA IS are structured around the IN-SYLVA standard.", + "part2": "This standard is composed of metadata fields. A metadata record is therefore made up of a series of fields accompanied by their value.", + "part3": "With this part of the interface you will be able to search for metadata records (previously loaded via the portal), by defining a certain number of criteria.", + "part4": "By default the \"search\" interface opens to a \"plain text\" search, ie the records returned in the result are those which, in one of the field values, contains the supplied character string.", + "part5": "A click on the Advanced search button gives access to a more complete form via which you can do more precise searches on one or more targeted fields.", + "part6": "Click on the \"Search\" tab to access the search interface." + } } diff --git a/public/locales/fr/home.json b/public/locales/fr/home.json index 6f2a42e..f9bdcd6 100644 --- a/public/locales/fr/home.json +++ b/public/locales/fr/home.json @@ -1,7 +1,11 @@ { - "pageTitle": "Bienvenue sur la page d'accueil du module de recherche du SI In-Sylva", - "description": { - "part1": "rezer", - "part2": "rezer" + "pageTitle": "Bienvenue sur la page d'accueil du module de recherche du Système d'Information In-Sylva", + "searchToolDescription": { + "part1": "Il est important de rappeler que les métadonnées stockées dans le SI In-Sylva sont structurées autour du standard établi par In-Sylva.", + "part2": "Il est composé de champs de métadonnées. Une fiche de métadonnées est donc constituée d'une série de champs accompagnés de leur valeur.", + "part3": "Cette interface vous permettra de rechercher des fiches de métadonnées (chargées au préalable par le Portal), en définissant un certain nombre de critères.", + "part4": "L'interface \"Recherche\" ouvre une zone de texte de \"Recherche basique\". Les résultats correspondent aux fiches de métadonnées contenant, dans un de leurs champs, la chaîne de caractère renseignée.", + "part5": "Un click sur le bouton \"Recherche avancée\" vous permets d'accéder à un formulaire plus complet qui vous permettra des recherches plus précises sur un ou plusieurs champs donnés.", + "part6": "Clickez sur l'onglet \"Recherche\" pour accéder à l'interface de recherche." } } diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js index 9eb375b..28040d1 100644 --- a/src/pages/home/Home.js +++ b/src/pages/home/Home.js @@ -3,7 +3,6 @@ import { EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, EuiTitle, } from '@elastic/eui'; import { useTranslation } from 'react-i18next'; @@ -20,37 +19,19 @@ const Home = () => { <h2>{t('pageTitle')}</h2> </EuiTitle> <br /> - <p> - As a reminder, it should be remembered that the metadata stored in IN-SYLVA - IS are structured around the IN-SYLVA standard. - </p> + <p>{t('searchToolDescription.part1')}</p> <br /> - <p> - This standard is composed of metadata fields. A metadata record is therefore - made up of a series of fields accompanied by their value. - </p> + <p>{t('searchToolDescription.part2')}</p> <br /> - <p> - With this part of the interface you will be able to search for metadata - records (previously loaded via the portal), by defining a certain number of - criteria. - </p> + <p>{t('searchToolDescription.part3')}</p> <br /> - <p> - By default the "search" interface opens to a "plain text" search, ie the - records returned in the result are those which, in one of the field values, - contains the supplied character string. - </p> + <p>{t('searchToolDescription.part4')}</p> <br /> - <p> - A click on the Advanced search button gives access to a more complete form - via which you can do more precise searches on one or more targeted fields. - </p> + <p>{t('searchToolDescription.part5')}</p> <br /> - <p>Click on the "Search" tab to access the search interface.</p> + <p>{t('searchToolDescription.part6')}</p> </EuiPageContentHeaderSection> </EuiPageContentHeader> - <EuiPageContentBody></EuiPageContentBody> </EuiPageContent> </> ); -- GitLab From 6939f712a291673f9913e16176c2b4e93c205691 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Mon, 29 Apr 2024 15:12:44 +0200 Subject: [PATCH 06/17] added full translation for Header and HeaderUserMenu --- public/locales/en/common.json | 3 ++- public/locales/en/header.json | 11 +++++++++-- public/locales/fr/common.json | 3 ++- public/locales/fr/header.json | 11 +++++++++-- src/components/Header/Header.js | 6 +++--- src/components/Header/HeaderUserMenu.js | 16 +++++++++++----- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 66bbf9e..deda3d5 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -2,5 +2,6 @@ "languages": { "en": "English", "fr": "French" - } + }, + "inSylvaLogoAlt": "In-Sylva logo" } diff --git a/public/locales/en/header.json b/public/locales/en/header.json index 9342625..889a13d 100644 --- a/public/locales/en/header.json +++ b/public/locales/en/header.json @@ -1,4 +1,11 @@ { - "home": "Home", - "search": "Search" + "tabs": { + "home": "Home", + "search": "Search" + }, + "userMenu": { + "title": "User profile", + "editProfileButton": "Edit profile", + "logOutButton": "Log out" + } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 74f9cd8..fa609bd 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -2,5 +2,6 @@ "languages": { "en": "Anglais", "fr": "Français" - } + }, + "inSylvaLogoAlt": "Logo In-Sylva" } diff --git a/public/locales/fr/header.json b/public/locales/fr/header.json index 73052c5..a5e28a5 100644 --- a/public/locales/fr/header.json +++ b/public/locales/fr/header.json @@ -1,4 +1,11 @@ { - "home": "Page d'accueil", - "search": "Recherche" + "tabs": { + "home": "Page d'accueil", + "search": "Recherche" + }, + "userMenu": { + "title": "Profil utilisateur", + "editProfileButton": "Modifier mon profil", + "logOutButton": "Déconnexion" + } } diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index ed7ce1f..d44347a 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -29,7 +29,7 @@ const structure = [ ]; const Header = () => { - const { t } = useTranslation('header'); + const { t } = useTranslation(['header', 'common']); return ( <> @@ -41,13 +41,13 @@ const Header = () => { src={logoInSylva} width="75" height="45" - alt="Logo INRAE" + alt={t('common:inSylvaLogoAlt')} /> </EuiHeaderSectionItem> <EuiHeaderLinks> {structure.map((link) => ( <EuiHeaderLink iconType="empty" key={link.id}> - <Link to={link.href}>{t(link.label)}</Link> + <Link to={link.href}>{t(`tabs.${link.label}`)}</Link> </EuiHeaderLink> ))} </EuiHeaderLinks> diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js index 122e7a2..2235573 100644 --- a/src/components/Header/HeaderUserMenu.js +++ b/src/components/Header/HeaderUserMenu.js @@ -11,8 +11,10 @@ import { } from '@elastic/eui'; import { signOut } from '../../context/UserContext'; import { findOneUser } from '../../actions/user'; +import { useTranslation } from 'react-i18next'; const HeaderUserMenu = () => { + const { t } = useTranslation('header'); const [isOpen, setIsOpen] = useState(false); const [user, setUser] = useState({}); @@ -41,8 +43,8 @@ const HeaderUserMenu = () => { size="s" onClick={onMenuButtonClick} iconType="user" - title="User profile" - aria-label="User profile" + title={t('userMenu.title')} + aria-label={t('userMenu.title')} /> ); @@ -56,7 +58,7 @@ const HeaderUserMenu = () => { closePopover={closeMenu} panelPaddingSize="none" > - <div style={{ width: 320 }}> + <div> <EuiFlexGroup gutterSize="m" className="euiHeaderProfile" responsive={false}> <EuiFlexItem grow={false}> <EuiAvatar name={user.username} size="xl" /> @@ -68,10 +70,14 @@ const HeaderUserMenu = () => { <EuiFlexItem> <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem grow={false}> - <EuiLink href="#/app/profile">Edit profile</EuiLink> + <EuiLink href="#/app/profile"> + {t('userMenu.editProfileButton')} + </EuiLink> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiLink onClick={() => signOut()}>Log out</EuiLink> + <EuiLink onClick={() => signOut()}> + {t('userMenu.logOutButton')} + </EuiLink> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> -- GitLab From 3efae1c0445405166e02244ac58e2f38a05f47a9 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Mon, 29 Apr 2024 15:36:58 +0200 Subject: [PATCH 07/17] added full translations for Maps tab --- public/locales/en/maps.json | 6 ++++++ public/locales/fr/maps.json | 6 ++++++ src/pages/maps/SearchMap.js | 18 +++++++++--------- src/pages/maps/styles.js | 4 ++-- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/public/locales/en/maps.json b/public/locales/en/maps.json index 2e100cb..08ae921 100644 --- a/public/locales/en/maps.json +++ b/public/locales/en/maps.json @@ -3,5 +3,11 @@ "layersTableHeaders": { "cartography": "Cartography", "data": "Data" + }, + "layersTable": { + "openStreetMap": "Open Street Map", + "bingAerial": "Bing Aerial", + "IGN": "IGN map", + "queryResults": "Query results" } } diff --git a/public/locales/fr/maps.json b/public/locales/fr/maps.json index afe4298..a03e165 100644 --- a/public/locales/fr/maps.json +++ b/public/locales/fr/maps.json @@ -3,5 +3,11 @@ "layersTableHeaders": { "cartography": "Cartographie", "data": "Données" + }, + "layersTable": { + "openStreetMap": "Open Street Map", + "bingAerial": "Bing carte", + "IGN": "Plan IGN", + "queryResults": "Résultats de la requête" } } diff --git a/src/pages/maps/SearchMap.js b/src/pages/maps/SearchMap.js index 64a2beb..db06894 100644 --- a/src/pages/maps/SearchMap.js +++ b/src/pages/maps/SearchMap.js @@ -2,11 +2,12 @@ import React, { useState, useEffect } from 'react'; import { Map, View } from 'ol'; import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'; //import {fromLonLat, get as getProjection} from 'ol/proj.js'; -// import TileLayer from 'ol/layer/Tile'; +//import TileLayer from 'ol/layer/Tile'; import ImageLayer from 'ol/layer/Image'; import GeoJSON from 'ol/format/GeoJSON'; import ImageWMS from 'ol/source/ImageWMS'; //import { Circle as CircleStyle, Fill, Stroke, Style, Text, Icon } from 'ol/style'; +import { Fill, Stroke, Style, Text, Icon } from 'ol/style'; import { toStringXY } from 'ol/coordinate'; import SourceOSM from 'ol/source/OSM'; import BingMaps from 'ol/source/BingMaps'; @@ -14,7 +15,6 @@ import { Vector as VectorSource } from 'ol/source'; import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; import { getWidth } from 'ol/extent'; -import { Fill, Stroke, Style, Text, Icon } from 'ol/style'; import { Circle, Point, Polygon } from 'ol/geom'; import Feature from 'ol/Feature'; import * as proj from 'ol/proj'; @@ -218,7 +218,7 @@ const SearchMap = (props) => { <> <div id="map" style={styles.mapContainer}></div> <div id="layertree"> - <table style={styles.legendTable}> + <table style={styles.layersTable}> <thead> <tr> <th>{t('layersTableHeaders.cartography')}</th> @@ -227,12 +227,12 @@ const SearchMap = (props) => { </thead> <tbody> <tr> - <td style={styles.legendTableCells}> + <td style={styles.layersTableCells}> <ul> <li> <EuiCheckbox id={htmlIdGenerator()()} - label="Open Street Map" + label={t('layersTable.openStreetMap')} checked={mapLayersVisibility[getLayerIndex('osm-layer')]} onChange={(e) => toggleLayer('osm-layer')} /> @@ -240,7 +240,7 @@ const SearchMap = (props) => { <li> <EuiCheckbox id={htmlIdGenerator()()} - label="Bing Aerial" + label={t('layersTable.bingAerial')} checked={mapLayersVisibility[getLayerIndex('Bing Aerial')]} onChange={(e) => toggleLayer('Bing Aerial')} /> @@ -248,7 +248,7 @@ const SearchMap = (props) => { <li> <EuiCheckbox id={htmlIdGenerator()()} - label="PLAN IGN" + label={t('maps:layersTable.IGN')} checked={mapLayersVisibility[getLayerIndex('IGN')]} onChange={(e) => toggleLayer('IGN')} /> @@ -273,10 +273,10 @@ const SearchMap = (props) => { */} </ul> </td> - <td style={styles.legendTableCells}> + <td style={styles.layersTableCells}> <EuiCheckbox id={htmlIdGenerator()()} - label="Query result" + label={t('layersTable.queryResults')} checked={mapLayersVisibility[getLayerIndex('query_results')]} onChange={(e) => toggleLayer('query_results')} /> diff --git a/src/pages/maps/styles.js b/src/pages/maps/styles.js index a991862..c1b8b54 100644 --- a/src/pages/maps/styles.js +++ b/src/pages/maps/styles.js @@ -106,10 +106,10 @@ const styles = { layerTree: { cursor: 'pointer', }, - legendTable: { + layersTable: { margin: '20px', }, - legendTableCells: { + layersTableCells: { padding: '10px', }, }; -- GitLab From fb013c13b3be49ff05f4a8400253dbdc1b4c88c8 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 30 Apr 2024 15:00:52 +0200 Subject: [PATCH 08/17] added full translations for profile page --- public/locales/en/common.json | 6 ++- public/locales/en/profile.json | 22 +++++++- public/locales/en/validation.json | 4 +- public/locales/fr/common.json | 6 ++- public/locales/fr/profile.json | 22 +++++++- public/locales/fr/validation.json | 4 +- src/pages/profile/Profile.js | 90 ++++++++++++++++++------------- src/pages/profile/styles.js | 8 +++ 8 files changed, 118 insertions(+), 44 deletions(-) create mode 100644 src/pages/profile/styles.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index deda3d5..48a5dcf 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -3,5 +3,9 @@ "en": "English", "fr": "French" }, - "inSylvaLogoAlt": "In-Sylva logo" + "inSylvaLogoAlt": "In-Sylva logo", + "validationActions": { + "cancel": "Cancel", + "send": "Send" + } } diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 7c610a5..76ab2db 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -1,3 +1,23 @@ { - "pageTitle": "Profile management" + "pageTitle": "Profile management", + "groups": { + "groupsList": "Group list", + "groupName": "Name", + "groupDescription": "Description" + }, + "requestsList": { + "requestsList": "Requests list", + "requestsMessage": "Message", + "processed": "Processed", + "cancelRequest": "Cancel this request" + }, + "groupRequests": { + "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." + }, + "roleRequests": { + "requestRoleAssignment": "Request an application role", + "currentRole": "You currently have (or have a pending request for) this role:" + } } diff --git a/public/locales/en/validation.json b/public/locales/en/validation.json index 0967ef4..b12bd6a 100644 --- a/public/locales/en/validation.json +++ b/public/locales/en/validation.json @@ -1 +1,3 @@ -{} +{ + "requestSent": "Your request has been sent to the administrators." +} diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index fa609bd..74be05d 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -3,5 +3,9 @@ "en": "Anglais", "fr": "Français" }, - "inSylvaLogoAlt": "Logo In-Sylva" + "inSylvaLogoAlt": "Logo In-Sylva", + "validationActions": { + "cancel": "Annuler", + "send": "Envoyer" + } } diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json index 983d813..9f30ddb 100644 --- a/public/locales/fr/profile.json +++ b/public/locales/fr/profile.json @@ -1,3 +1,23 @@ { - "pageTitle": "Gestion du profil" + "pageTitle": "Gestion du profil", + "groups": { + "groupsList": "Liste des groupes", + "groupName": "Nom", + "groupDescription": "Description" + }, + "requestsList": { + "requestsList": "Liste des requêtes", + "requestsMessage": "Message", + "processed": "Traitée", + "cancelRequest": "Annuler cette requête" + }, + "groupRequests": { + "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." + }, + "roleRequests": { + "requestRoleAssignment": "Demander un rôle", + "currentRole": "Votre rôle actuel (ou demande en cours):" + } } diff --git a/public/locales/fr/validation.json b/public/locales/fr/validation.json index 0967ef4..0f29592 100644 --- a/public/locales/fr/validation.json +++ b/public/locales/fr/validation.json @@ -1 +1,3 @@ -{} +{ + "requestSent": "Votre requête à bien été envoyée." +} diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js index 3e8c303..89bd3a7 100644 --- a/src/pages/profile/Profile.js +++ b/src/pages/profile/Profile.js @@ -24,9 +24,10 @@ import { deleteUserRequest, } from '../../actions/user'; import { useTranslation } from 'react-i18next'; +import styles from './styles'; const Profile = () => { - const { t } = useTranslation('profile'); + const { t } = useTranslation(['profile', 'common', 'validation']); const [user, setUser] = useState({}); const [userRole, setUserRole] = useState(''); const [groups, setGroups] = useState([]); @@ -65,8 +66,8 @@ const Profile = () => { }, [userGroups]); const groupColumns = [ - { field: 'label', name: 'Group Name', width: '30%' }, - { field: 'description', name: 'Group description' }, + { field: 'label', name: t('groups.groupName'), width: '30%' }, + { field: 'description', name: t('groups.groupDescription') }, ]; const getUserRoles = () => { @@ -109,8 +110,8 @@ const Profile = () => { const requestActions = [ { - name: 'Cancel', - description: 'Cancel this request', + name: t('common:validationActions.cancel'), + description: t('requestsList.cancelRequest'), icon: 'trash', type: 'icon', onClick: onDeleteRequest, @@ -118,9 +119,13 @@ const Profile = () => { ]; const requestsColumns = [ - { field: 'request_message', name: 'Message', width: '90%' }, - { field: 'is_processed', name: 'Processed' }, - { name: 'Delete', actions: requestActions }, + { + field: 'request_message', + name: t('requestsList.requestsMessage'), + width: '85%', + }, + { field: 'is_processed', name: t('requestsList.processed') }, + { name: t('common:validationActions.cancel'), actions: requestActions }, ]; const getUserGroupLabels = () => { @@ -144,6 +149,30 @@ const Profile = () => { ); }; + 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 = () => { + const 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 ( <> <EuiPageContent> @@ -157,29 +186,28 @@ const Profile = () => { <EuiPageContentBody> <EuiForm component="form"> <EuiTitle size="s"> - <h3>Group list</h3> + <h3>{t('groups.groupsList')}</h3> </EuiTitle> <EuiFormRow fullWidth label=""> <EuiBasicTable items={groups} columns={groupColumns} /> </EuiFormRow> <EuiSpacer size="l" /> <EuiTitle size="s"> - <h3>Requests list</h3> + <h3>{t('requestsList.requestsList')}</h3> </EuiTitle> <EuiFormRow fullWidth label=""> <EuiBasicTable items={userRequests} columns={requestsColumns} /> </EuiFormRow> <EuiSpacer size="l" /> <EuiTitle size="s"> - <h3>Request group assignment modifications</h3> + <h3>{t('groupRequests.requestGroupAssignment')}</h3> </EuiTitle> {getUserGroupLabels() ? ( - <p> - You currently belong to (or have a pending demand for) these groups :{' '} - {getUserGroupLabels()}{' '} - </p> + <p + style={styles.currentRoleOrGroupText} + >{`${t('groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p> ) : ( - <p>You currently belong to no group</p> + <p>{t('groupRequests.noGroup')}</p> )} <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> <EuiComboBox @@ -196,28 +224,20 @@ const Profile = () => { <EuiSpacer size="m" /> <EuiButton onClick={() => { - if (userGroups) { - const groupList = []; - 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('Your group request has been sent to the administrators.'); - } - getUserRequests(); + onSendGroupRequest(); }} fill > - Send request + {t('common:validationActions.send')} </EuiButton> <EuiSpacer size="l" /> <EuiTitle size="s"> - <h3>Request an application role</h3> + <h3>{t('roleRequests.requestRoleAssignment')}</h3> </EuiTitle> {userRole ? ( - <p>Your current role is (or have a pending demand for) {userRole}</p> + <p + style={styles.currentRoleOrGroupText} + >{`${t('roleRequests.currentRole')} ${userRole}`}</p> ) : ( <></> )} @@ -234,17 +254,11 @@ const Profile = () => { <EuiSpacer size="m" /> <EuiButton onClick={() => { - 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('Your role request has been sent to the administrators.'); - } - getUserRequests(); + onSendRoleRequest(); }} fill > - Send request + {t('common:validationActions.send')} </EuiButton> </EuiForm> </EuiPageContentBody> diff --git a/src/pages/profile/styles.js b/src/pages/profile/styles.js new file mode 100644 index 0000000..4c2776e --- /dev/null +++ b/src/pages/profile/styles.js @@ -0,0 +1,8 @@ +const style = { + currentRoleOrGroupText: { + marginTop: '10px', + marginBottom: '10px', + }, +}; + +export default style; -- GitLab From 1eaea0274d3c0855ef1a9397fc75c6fda3b6d6a3 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 7 May 2024 11:28:30 +0200 Subject: [PATCH 09/17] [i18n config] updated default namespace to correct bug --- src/i18n.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n.js b/src/i18n.js index 65a0362..236acc5 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -8,7 +8,8 @@ i18n .init({ lng: 'fr', fallbackLng: 'fr', - defaultNS: 'home', + ns: 'common', + defaultNS: 'common', debug: true, load: 'languageOnly', loadPath: 'locales/{{lng}}/{{ns}}.json', -- GitLab From b41b40819be9fca874d4e647732f950a86584f43 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 7 May 2024 11:30:58 +0200 Subject: [PATCH 10/17] [search]: divided basic search and advanced search into separate components --- src/pages/results/Results.js | 2 +- .../search/AdvancedSearch/AdvancedSearch.js | 1627 +++++++++++++++ src/pages/search/BasicSearch/BasicSearch.js | 89 + src/pages/search/Search.js | 1795 +---------------- src/pages/search/styles.js | 15 + 5 files changed, 1818 insertions(+), 1710 deletions(-) create mode 100644 src/pages/search/AdvancedSearch/AdvancedSearch.js create mode 100644 src/pages/search/BasicSearch/BasicSearch.js create mode 100644 src/pages/search/styles.js diff --git a/src/pages/results/Results.js b/src/pages/results/Results.js index 0c61fce..e1dd381 100644 --- a/src/pages/results/Results.js +++ b/src/pages/results/Results.js @@ -38,7 +38,7 @@ const changeFlyoutState = (array, index, value, defaultValue) => { return newArray; }; -const Results = (searchResults, search, basicSearch) => { +const Results = ({ searchResults, search, basicSearch }) => { const { t } = useTranslation('results'); const [resultsCol, setResultsCol] = useState([]); const [results, setResults] = useState([]); diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js new file mode 100644 index 0000000..f8db71a --- /dev/null +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -0,0 +1,1627 @@ +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiComboBox, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiGlobalToastList, + EuiHealth, + EuiIcon, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiPanel, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiProgress, + EuiRadioGroup, + EuiSelect, + EuiSpacer, + EuiSwitch, + EuiTextArea, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import { + changeNameToLabel, + createAdvancedQueriesBySource, + getFieldsBySection, + getSections, + removeArrayElement, + SearchField, + updateArrayElement, + updateSearchFieldValues, +} from '../../../Utils'; +import { getQueryCount, searchQuery } from '../../../actions/source'; +import { DateOptions, NumericOptions, Operators } from '../Data'; +import TextField from '@material-ui/core/TextField'; +import { addUserHistory } from '../../../actions/user'; +import { fetchHistory } from '../Search'; + +const updateSources = ( + searchFields, + sources, + setSelectedSources, + setAvailableSources +) => { + let updatedSources = []; + let availableSources = []; + let noPrivateField = true; + //search for policy fields to filter sources + searchFields.forEach((field) => { + if (field.isValidated) { + //if sources haven't already been filtered + if (noPrivateField && !updatedSources.length) { + availableSources = sources; + } else { + availableSources = updatedSources; + } + updatedSources = []; + field.sources.forEach((sourceId) => { + noPrivateField = false; + const source = availableSources.find((src) => src.id === sourceId); + if (source && !updatedSources.includes(source)) updatedSources.push(source); + }); + } + }); + setSelectedSources(updatedSources); + if (noPrivateField && !updatedSources.length) { + setAvailableSources(sources); + } else { + setAvailableSources(updatedSources); + } +}; + +const fieldValuesToString = (field) => { + let strValues = ''; + switch (field.type) { + case 'Numeric': + field.values.forEach((element) => { + switch (element.option) { + case 'between': + strValues = `${strValues} ${element.value1} <= ${field.name} <= ${element.value2} or `; + break; + default: + strValues = `${strValues} ${field.name} ${element.option} ${element.value1} or `; + } + }); + if (strValues.endsWith('or ')) + strValues = strValues.substring(0, strValues.length - 4); + break; + case 'Date': + field.values.forEach((element) => { + switch (element.option) { + case 'between': + strValues = `${strValues} ${element.startDate} <= ${field.name} <= ${element.endDate} or `; + break; + default: + strValues = `${strValues} ${field.name} ${element.option} ${element.startDate} or `; + } + }); + if (strValues.endsWith(' or ')) + strValues = strValues.substring(0, strValues.length - 4); + break; + case 'List': + strValues = `${strValues} ${field.name} = `; + field.values.forEach((element) => { + strValues = `${strValues} ${element.label}, `; + }); + if (strValues.endsWith(', ')) + strValues = strValues.substring(0, strValues.length - 2); + break; + //type : text + default: + strValues = `${strValues} ${field.name} = ${field.values}`; + } + return strValues; +}; + +const addHistory = ( + kcID, + search, + searchName, + searchFields, + searchDescription, + setUserHistory +) => { + addUserHistory( + sessionStorage.getItem('user_id'), + search, + searchName, + searchFields, + searchDescription + ).then(() => { + fetchHistory(setUserHistory); + }); +}; + +const updateSearch = (setSearch, searchFields, selectedOperatorId, setSearchCount) => { + let searchText = ''; + searchFields.forEach((field) => { + if (field.isValidated) { + searchText = + searchText + + `{${fieldValuesToString(field)} } ${Operators[selectedOperatorId].value.toUpperCase()} `; + } + }); + if (searchText.endsWith(' AND ')) { + searchText = searchText.substring(0, searchText.length - 5); + } else if (searchText.endsWith(' OR ')) { + searchText = searchText.substring(0, searchText.length - 4); + } + setSearchCount(); + setSearch(searchText); +}; + +const HistorySelect = ( + sources, + setAvailableSources, + setSelectedSources, + setSearch, + searchFields, + selectedOperatorId, + userHistory, + setUserHistory, + setSearchFields, + setSearchCount, + setFieldCount, + selectedSavedSearch, + setSelectedSavedSearch, + historySelectError, + setHistorySelectError +) => { + if (Object.keys(userHistory).length !== 0) { + const onHistoryChange = (selectedSavedSearch) => { + setHistorySelectError(undefined); + if (!!selectedSavedSearch[0].query) { + setSelectedSavedSearch(selectedSavedSearch); + setSearch(selectedSavedSearch[0].query); + setSearchCount(); + setFieldCount([]); + } + if (!!selectedSavedSearch[0].ui_structure) { + updateSources( + selectedSavedSearch[0].ui_structure, + sources, + setSelectedSources, + setAvailableSources + ); + setSearchFields(selectedSavedSearch[0].ui_structure); + } + }; + + const onHistorySearchChange = (value, hasMatchingOptions) => { + setHistorySelectError( + value.length === 0 || hasMatchingOptions + ? undefined + : `"${value}" is not a valid option` + ); + }; + + return ( + <> + <EuiFormRow + error={historySelectError} + isInvalid={historySelectError !== undefined} + > + <EuiComboBox + placeholder={'searchHistory.placeholder'} + singleSelection={{ asPlainText: true }} + options={userHistory} + selectedOptions={selectedSavedSearch} + onChange={onHistoryChange} + onSearchChange={onHistorySearchChange} + /> + </EuiFormRow> + </> + ); + } +}; + +const SearchBar = ({ + isLoading, + setIsLoading, + search, + setSearch, + setSearchResults, + searchFields, + setSearchFields, + searchName, + setSearchName, + searchDescription, + setSearchDescription, + readOnlyQuery, + setReadOnlyQuery, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources, + standardFields, + sources, + setSelectedTabNumber, + searchCount, + setSearchCount, + setFieldCount, + isSaveSearchModalOpen, + setIsSaveSearchModalOpen, + userHistory, + setUserHistory, + selectedSavedSearch, + setSelectedSavedSearch, + historySelectError, + setHistorySelectError, + selectedOperatorId, + createEditableQueryToast, +}) => { + const closeSaveSearchModal = () => { + setIsSaveSearchModalOpen(false); + }; + + const onSendAdvancedSearch = () => { + if (search.trim()) { + setIsLoading(true); + const queriesWithIndices = createAdvancedQueriesBySource( + standardFields, + search, + selectedSources, + availableSources + ); + searchQuery(queriesWithIndices).then((result) => { + setSearchResults(result); + setSelectedTabNumber(1); + setIsLoading(false); + }); + } + }; + + let saveSearchModal; + + if (isSaveSearchModalOpen) { + saveSearchModal = ( + <EuiOverlayMask> + <EuiModal onClose={closeSaveSearchModal} initialFocus="[name=searchName]"> + <EuiModalHeader> + <EuiModalHeaderTitle>Save search</EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <EuiForm> + <EuiFormRow label="Search name"> + <EuiFieldText + name="searchName" + value={searchName} + onChange={(e) => { + setSearchName(e.target.value); + }} + /> + </EuiFormRow> + <EuiFormRow label="Description (optional)"> + <EuiTextArea + value={searchDescription} + onChange={(e) => setSearchDescription(e.target.value)} + placeholder="Search description..." + fullWidth + compressed + /> + </EuiFormRow> + </EuiForm> + </EuiModalBody> + + <EuiModalFooter> + <EuiButtonEmpty + onClick={() => { + closeSaveSearchModal(); + }} + > + Cancel + </EuiButtonEmpty> + <EuiButton + onClick={() => { + if (!!searchName) { + addHistory( + sessionStorage.getItem('user_id'), + search, + searchName, + searchFields, + searchDescription, + setUserHistory + ); + setSearchName(''); + setSearchDescription(''); + closeSaveSearchModal(); + } + }} + fill + > + Save + </EuiButton> + </EuiModalFooter> + </EuiModal> + </EuiOverlayMask> + ); + } + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTextArea + readOnly={readOnlyQuery} + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder="Add fields..." + fullWidth + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + size="s" + fill + onClick={() => { + onSendAdvancedSearch(); + }} + > + Search + </EuiButton> + <EuiSpacer size="s" /> + {isNaN(searchCount) ? ( + <></> + ) : ( + <> + <EuiTextColor + color="secondary" + style={{ display: 'flex', justifyContent: 'center' }} + > + {searchCount} {searchCount === 1 ? 'result' : 'results'} + </EuiTextColor> + <EuiSpacer size="s" /> + </> + )} + <EuiButton + size="s" + onClick={() => { + if (!!search) { + const queriesWithIndices = createAdvancedQueriesBySource( + standardFields, + search, + selectedSources, + availableSources + ); + getQueryCount(queriesWithIndices).then((result) => { + if (result || result === 0) setSearchCount(result); + }); + } + }} + > + Count results + </EuiButton> + <EuiSpacer size="s" /> + <EuiButton + size="s" + onClick={() => { + setIsSaveSearchModalOpen(true); + }} + > + Save search + </EuiButton> + {saveSearchModal} + <EuiSpacer size="s" /> + <EuiSwitch + compressed + label={'Editable'} + checked={!readOnlyQuery} + onChange={() => { + setReadOnlyQuery(!readOnlyQuery); + if (readOnlyQuery) { + createEditableQueryToast(); + } + }} + /> + </EuiFlexItem> + </EuiFlexGroup> + {isLoading && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress postion="fixed" size="l" color="accent" /> + </EuiFlexItem> + </EuiFlexGroup> + )} + <EuiSpacer size="s" /> + <EuiFlexGroup> + <EuiFlexItem> + {HistorySelect( + sources, + setAvailableSources, + setSelectedSources, + setSearch, + searchFields, + selectedOperatorId, + userHistory, + setUserHistory, + setSearchFields, + setSearchCount, + setFieldCount, + selectedSavedSearch, + setSelectedSavedSearch, + historySelectError, + setHistorySelectError + )} + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +const PopoverSelect = ( + standardFields, + setStandardFields, + searchFields, + setSearchFields, + selectedField, + setSelectedField, + selectedSection, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen +) => { + const handleAddfield = () => { + if (!!selectedField[0]) { + const field = standardFields.find( + (item) => + item.field_name.replace(/_|\./g, ' ') === + selectedSection[0].label + ' ' + selectedField[0].label + ); + switch (field.field_type) { + case 'Text': + setSearchFields([ + ...searchFields, + new SearchField(field.field_name, field.field_type, '', false, field.sources), + ]); + break; + case 'List': + setSearchFields([ + ...searchFields, + new SearchField(field.field_name, field.field_type, [], false, field.sources), + ]); + break; + default: + setSearchFields([ + ...searchFields, + new SearchField( + field.field_name, + field.field_type, + [{}], + false, + field.sources + ), + ]); + } + } + }; + + const selectField = () => { + const renderOption = (option, searchValue, contentClassName) => { + const { label, color } = option; + return <EuiHealth color={color}>{label}</EuiHealth>; + }; + if (selectedSection.length) { + return ( + <> + <EuiComboBox + placeholder="Select a field" + singleSelection={{ asPlainText: true }} + options={getFieldsBySection(standardFields, selectedSection[0])} + selectedOptions={selectedField} + onChange={(selected) => setSelectedField(selected)} + isClearable={true} + renderOption={renderOption} + /> + <EuiPopoverFooter> + <EuiButton + size="s" + onClick={() => { + handleAddfield(); + setIsPopoverSelectOpen(false); + setSelectedSection([]); + setSelectedField([]); + }} + > + Add this field + </EuiButton> + </EuiPopoverFooter> + </> + ); + } + }; + + return ( + <EuiPopover + panelPaddingSize="s" + button={ + <EuiButton + iconType="listAdd" + iconSide="left" + onClick={() => setIsPopoverSelectOpen(!isPopoverSelectOpen)} + > + Add field + </EuiButton> + } + isOpen={isPopoverSelectOpen} + closePopover={() => setIsPopoverSelectOpen(false)} + > + <div style={{ width: 'intrinsic', minWidth: 240 }}> + <EuiPopoverTitle>Select a field</EuiPopoverTitle> + <EuiComboBox + placeholder="Select a section" + singleSelection={{ asPlainText: true }} + options={getSections(standardFields)} + selectedOptions={selectedSection} + onChange={(selected) => { + setSelectedSection(selected); + setSelectedField([]); + }} + isClearable={false} + /> + </div> + {selectField()} + </EuiPopover> + ); +}; + +const PopoverValueContent = ( + index, + standardFields, + setStandardFields, + searchFields, + setSearchFields, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + fieldCount, + setFieldCount, + isPopoverValueOpen, + setIsPopoverValueOpen, + selectedOperatorId, + datePickerStyles, + createPolicyToast, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources +) => { + const onValueSearchChange = (value, hasMatchingOptions) => { + setValueError( + value.length === 0 || hasMatchingOptions + ? undefined + : `"${value}" is not a valid option` + ); + }; + + const validateFieldValues = () => { + let fieldValues; + if (Array.isArray(searchFields[index].values)) { + fieldValues = []; + searchFields[index].values.forEach((value) => { + if (!!value) { + fieldValues.push(value); + } + }); + } else { + fieldValues = searchFields[index].values; + } + + const updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + fieldValues, + true, + searchFields[index].sources + ) + ); + setSearchFields(updatedSearchFields); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + setFieldCount(updateArrayElement(fieldCount, index)); + if (searchFields[index].sources.length) { + const filteredSources = []; + searchFields[index].sources.forEach((sourceId) => { + let source; + if (selectedSources.length) { + source = selectedSources.find((src) => src.id === sourceId); + } else { + source = availableSources.find((src) => src.id === sourceId); + } + if (source) { + filteredSources.push(source); + } + }); + setAvailableSources(filteredSources); + setSelectedSources(filteredSources); + createPolicyToast(); + } + }; + + const invalidateFieldValues = () => { + const updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + searchFields[index].values, + false, + searchFields[index].sources + ) + ); + setSearchFields(updatedSearchFields); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + }; + + const ValuePopoverFooter = (i) => { + if (i === searchFields[index].values.length - 1) { + return ( + <EuiPopoverFooter> + <EuiButton + size="s" + onClick={() => { + setSearchFields( + updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + [...searchFields[index].values, {}], + false, + searchFields[index].sources + ) + ) + ); + }} + > + Add value + </EuiButton> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)); + }} + > + Validate + </EuiButton> + </EuiPopoverFooter> + ); + } + }; + + const addFieldValue = (i, selectedOption) => { + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { option: selectedOption }) + ) + ); + }; + + const getListFieldValues = () => { + const listFieldValues = []; + standardFields + .find((item) => item.field_name === searchFields[index].name) + .values.split(', ') + .sort() + .forEach((element) => { + listFieldValues.push({ label: element }); + }); + return listFieldValues; + }; + + switch (searchFields[index].type) { + case 'Text': + return ( + <> + <EuiFlexItem> + <EuiFieldText + placeholder={'Type values'} + value={searchFields[index].values} + onChange={(e) => + setSearchFields( + updateSearchFieldValues(searchFields, index, e.target.value) + ) + } + /> + </EuiFlexItem> + <EuiPopoverFooter> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen( + updateArrayElement(isPopoverValueOpen, index, false) + ); + }} + > + Validate + </EuiButton> + </EuiPopoverFooter> + </> + ); + case 'List': + return ( + <> + <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> + <EuiComboBox + placeholder={'Select values'} + options={getListFieldValues()} + selectedOptions={searchFields[index].values} + onChange={(selectedOptions) => { + setValueError(undefined); + setSearchFields( + updateSearchFieldValues(searchFields, index, selectedOptions) + ); + }} + onSearchChange={onValueSearchChange} + /> + </EuiFormRow> + <EuiPopoverFooter> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen( + updateArrayElement(isPopoverValueOpen, index, false) + ); + }} + > + Validate + </EuiButton> + </EuiPopoverFooter> + </> + ); + case 'Numeric': + const NumericValues = (i) => { + if (!!searchFields[index].values[i].option) { + switch (searchFields[index].values[i].option) { + case 'between': + return ( + <> + <EuiFlexItem> + <EuiFieldText + placeholder={'1st value'} + 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={'2nd value'} + 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={'Type value'} + 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> + ))} + </> + ); + 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="between" + 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="and" + 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> + ))} + </> + ); + default: + } +}; + +const PopoverValueButton = ( + index, + standardFields, + setStandardFields, + searchFields, + setSearchFields, + isPopoverValueOpen, + setIsPopoverValueOpen, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + fieldCount, + setFieldCount, + selectedOperatorId, + datePickerStyles, + createPolicyToast, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources +) => { + return ( + <EuiPopover + panelPaddingSize="s" + button={ + <EuiButtonIcon + size="s" + color="primary" + onClick={() => + setIsPopoverValueOpen( + updateArrayElement(isPopoverValueOpen, index, !isPopoverValueOpen[index]) + ) + } + iconType="documentEdit" + title="Give field values" + /> + } + isOpen={isPopoverValueOpen[index]} + closePopover={() => + setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)) + } + > + <div style={{ width: 240 }}> + {PopoverValueContent( + index, + standardFields, + setStandardFields, + searchFields, + setSearchFields, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + fieldCount, + setFieldCount, + isPopoverValueOpen, + setIsPopoverValueOpen, + selectedOperatorId, + datePickerStyles, + createPolicyToast, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources + )} + </div> + </EuiPopover> + ); +}; + +const FieldsPanel = ( + standardFields, + setStandardFields, + searchFields, + setSearchFields, + selectedField, + setSelectedField, + selectedSection, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, + isPopoverValueOpen, + setIsPopoverValueOpen, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + selectedOperatorId, + setSelectedOperatorId, + fieldCount, + setFieldCount, + availableSources, + setAvailableSources, + selectedSources, + setSelectedSources, + sources, + datePickerStyles, + createPolicyToast +) => { + const countFieldValues = (field, index) => { + const fieldStr = `{${fieldValuesToString(field)}}`; + const queriesWithIndices = createAdvancedQueriesBySource( + standardFields, + fieldStr, + selectedSources, + availableSources + ); + getQueryCount(queriesWithIndices).then((result) => { + if (result || result === 0) + setFieldCount(updateArrayElement(fieldCount, index, result)); + }); + }; + + const handleRemoveField = (index) => { + const updatedSearchFields = removeArrayElement(searchFields, index); + setSearchFields(updatedSearchFields); + updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + }; + + const handleClearValues = (index) => { + let updatedSearchFields = []; + switch (searchFields[index].type) { + case 'Text': + updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + '', + false, + searchFields[index].sources + ) + ); + break; + case 'List': + updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + [], + false, + searchFields[index].sources + ) + ); + break; + default: + updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + [{}], + false, + searchFields[index].sources + ) + ); + } + setSearchFields(updatedSearchFields); + updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); + setFieldCount(updateArrayElement(fieldCount, index)); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + }; + + if (standardFields === []) { + return <h2>Loading user fields...</h2>; + } + + return ( + <> + <EuiTitle size="xs"> + <h2>Field search</h2> + </EuiTitle> + <EuiPanel paddingSize="m"> + <EuiFlexGroup direction="column"> + {searchFields.map((field, index) => ( + <EuiPanel key={'field' + index} paddingSize="s"> + <EuiFlexItem grow={false}> + <EuiFlexGroup direction="row" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonIcon + size="s" + color="danger" + onClick={() => handleRemoveField(index)} + iconType="indexClose" + title="Remove field" + /> + </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"> + {fieldCount[index]}{' '} + {fieldCount[index] === 1 ? 'result' : 'results'} + </EuiTextColor> + </> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {field.isValidated ? ( + <> + <EuiButtonIcon + size="s" + onClick={() => countFieldValues(field, index)} + iconType="number" + title="Count results" + /> + </> + ) : ( + <></> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {field.isValidated ? ( + <> + <EuiButtonIcon + size="s" + color="danger" + onClick={() => handleClearValues(index)} + iconType="trash" + title="Clear values" + /> + </> + ) : ( + <></> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {PopoverValueButton( + index, + standardFields, + setStandardFields, + searchFields, + setSearchFields, + isPopoverValueOpen, + setIsPopoverValueOpen, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + fieldCount, + setFieldCount, + selectedOperatorId, + datePickerStyles, + createPolicyToast, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources + )} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiPanel> + ))} + </EuiFlexGroup> + <EuiSpacer size="l" /> + {PopoverSelect( + standardFields, + setStandardFields, + searchFields, + setSearchFields, + selectedField, + setSelectedField, + selectedSection, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, + fieldCount, + setFieldCount, + selectedSources, + setSelectedSources + )} + </EuiPanel> + <EuiSpacer size="s" /> + <EuiRadioGroup + options={Operators} + idSelected={selectedOperatorId} + onChange={(id) => { + setSelectedOperatorId(id); + updateSearch(setSearch, searchFields, id, setSearchCount); + }} + name="operators group" + legend={{ + children: <span>Search option</span>, + }} + /> + </> + ); +}; + +const SourceSelect = ( + availableSources, + selectedSources, + setSelectedSources, + sourceSelectError, + setSourceSelectError +) => { + if (Object.keys(availableSources).length !== 0) { + availableSources.forEach((source) => { + if (source.name) { + source = changeNameToLabel(source); + } + }); + + const onSourceChange = (selectedOptions) => { + setSourceSelectError(undefined); + setSelectedSources(selectedOptions); + }; + + const onSourceSearchChange = (value, hasMatchingOptions) => { + setSourceSelectError( + value.length === 0 || hasMatchingOptions + ? undefined + : `"${value}" is not a valid option` + ); + }; + return ( + <> + <EuiTitle size="xs"> + <h2>Partner sources</h2> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiFlexItem> + <EuiFormRow + error={sourceSelectError} + isInvalid={sourceSelectError !== undefined} + > + <EuiComboBox + placeholder="By default, all sources are selected" + options={availableSources} + selectedOptions={selectedSources} + onChange={onSourceChange} + onSearchChange={onSourceSearchChange} + /> + </EuiFormRow> + </EuiFlexItem> + </> + ); + } else { + return ( + <p> + <EuiIcon type="alert" color="danger" /> No source available ! + </p> + ); + } +}; + +const AdvancedSearch = ({ + isLoading, + setIsLoading, + search, + setSearch, + searchResults, + setSearchResults, + searchFields, + setSearchFields, + searchName, + setSearchName, + searchDescription, + setSearchDescription, + readOnlyQuery, + setReadOnlyQuery, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources, + standardFields, + setStandardFields, + sources, + setSelectedTabNumber, + searchCount, + setSearchCount, + setFieldCount, + isReadOnlyModalOpen, + setIsReadOnlyModalOpen, + isSaveSearchModalOpen, + setIsSaveSearchModalOpen, + userHistory, + setUserHistory, + selectedSavedSearch, + setSelectedSavedSearch, + historySelectError, + setHistorySelectError, + selectedOperatorId, + notificationToasts, + setNotificationToasts, + setIsAdvancedSearch, + isAdvancedSearch, + selectedField, + selectedSection, + setSelectedField, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, + setIsPopoverValueOpen, + isPopoverValueOpen, + valueError, + setValueError, + setSelectedOperatorId, + fieldCount, + sourceSelectError, + datePickerStyles, + setSourceSelectError, +}) => { + const createPolicyToast = () => { + const toast = { + title: 'Policy field selected', + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 15000, + text: ( + <> + <p>You selected a private field.</p> + <p> + Access to this field was granted for specific sources, which means that your + search will be restricted to those. + </p> + <p>Please check the sources list before searching.</p> + </> + ), + }; + setNotificationToasts(notificationToasts.concat(toast)); + }; + + const createEditableQueryToast = () => { + const toast = { + title: 'Proceed with caution', + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 15000, + text: ( + <> + <p> + Be aware that manually editing the query can spoil search results. The syntax + must be respected : + </p> + <ul> + Fields and their values should be put between brackets : { } - Make + sure every opened bracket is properly closed + </ul> + <ul> + "AND" and "OR" should be capitalized between different fields conditions and + lowercased within a field expression + </ul> + <ul>Make sure to check eventual typing mistakes</ul> + </> + ), + }; + setNotificationToasts(notificationToasts.concat(toast)); + }; + + const removeToast = (removedToast) => { + setNotificationToasts( + notificationToasts.filter((toast) => toast.id !== removedToast.id) + ); + }; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiButtonEmpty + onClick={() => { + setIsAdvancedSearch(!isAdvancedSearch); + }} + > + Switch to basic search + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + <SearchBar + isLoading={isLoading} + setIsLoading={setIsLoading} + search={search} + setSearch={setSearch} + searchResults={searchResults} + setSearchResults={setSearchResults} + searchFields={searchFields} + setSearchFields={setSearchFields} + searchName={searchName} + setSearchName={setSearchName} + searchDescription={searchDescription} + setSearchDescription={setSearchDescription} + readOnlyQuery={readOnlyQuery} + setReadOnlyQuery={setReadOnlyQuery} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + standardFields={standardFields} + sources={sources} + setSelectedTabNumber={setSelectedTabNumber} + searchCount={searchCount} + setSearchCount={setSearchCount} + setFieldCount={setFieldCount} + isReadOnlyModalOpen={isReadOnlyModalOpen} + setIsReadOnlyModalOpen={setIsReadOnlyModalOpen} + isSaveSearchModalOpen={isSaveSearchModalOpen} + setIsSaveSearchModalOpen={setIsSaveSearchModalOpen} + userHistory={userHistory} + setUserHistory={setUserHistory} + selectedSavedSearch={selectedSavedSearch} + setSelectedSavedSearch={setSelectedSavedSearch} + historySelectError={historySelectError} + setHistorySelectError={setHistorySelectError} + selectedOperatorId={selectedOperatorId} + createEditableQueryToast={createEditableQueryToast} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + {FieldsPanel( + standardFields, + setStandardFields, + searchFields, + setSearchFields, + selectedField, + setSelectedField, + selectedSection, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, + isPopoverValueOpen, + setIsPopoverValueOpen, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + selectedOperatorId, + setSelectedOperatorId, + fieldCount, + setFieldCount, + availableSources, + setAvailableSources, + selectedSources, + setSelectedSources, + sources, + datePickerStyles, + createPolicyToast + )} + <EuiSpacer size="s" /> + {SourceSelect( + availableSources, + selectedSources, + setSelectedSources, + sourceSelectError, + setSourceSelectError + )} + </EuiFlexItem> + </EuiFlexGroup> + <EuiGlobalToastList + toasts={notificationToasts} + dismissToast={removeToast} + toastLifeTimeMs={2500} + /> + </> + ); +}; + +export default AdvancedSearch; diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js new file mode 100644 index 0000000..20e7296 --- /dev/null +++ b/src/pages/search/BasicSearch/BasicSearch.js @@ -0,0 +1,89 @@ +import React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, +} from '@elastic/eui'; +import { createBasicQueriesBySource } from '../../../Utils'; +import { searchQuery } from '../../../actions/source'; + +const BasicSearch = ({ + standardFields, + availableSources, + selectedSources, + basicSearch, + setBasicSearch, + isLoading, + setIsAdvancedSearch, + isAdvancedSearch, + setIsLoading, + setSearchResults, + setSelectedTabNumber, +}) => { + const onFormSubmit = () => { + setIsLoading(true); + const queriesWithIndices = createBasicQueriesBySource( + standardFields, + basicSearch, + selectedSources, + availableSources + ); + searchQuery(queriesWithIndices).then((result) => { + setSearchResults(result); + setSelectedTabNumber(1); + setIsLoading(false); + }); + }; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiButtonEmpty + onClick={() => { + setIsAdvancedSearch(!isAdvancedSearch); + }} + > + Switch to advanced search + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + <form onSubmit={onFormSubmit}> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFieldSearch + value={basicSearch} + onChange={(e) => setBasicSearch(e.target.value)} + placeholder="Search..." + fullWidth + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton type="submit" fill isDisabled={isAdvancedSearch}> + Search + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </form> + {isLoading && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress postion="fixed" size="l" color="accent" /> + </EuiFlexItem> + </EuiFlexGroup> + )} + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +export default BasicSearch; diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 901e335..16d3043 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -1,161 +1,31 @@ import React, { useState, useEffect } from 'react'; import { - EuiProgress, - EuiRadioGroup, - EuiFieldText, - EuiPanel, - EuiPopover, - EuiPopoverTitle, - EuiPopoverFooter, EuiTabbedContent, - EuiFormRow, - EuiComboBox, EuiPageContentBody, EuiForm, - EuiTextArea, EuiFlexGroup, EuiFlexItem, - EuiFieldSearch, - EuiButton, - EuiButtonEmpty, - EuiSwitch, - EuiButtonIcon, - EuiIcon, EuiSpacer, EuiPageContent, EuiPageContentHeader, EuiTitle, EuiPageContentHeaderSection, - EuiTextColor, - EuiOverlayMask, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiSelect, - EuiGlobalToastList, - EuiHealth, } from '@elastic/eui'; -import { makeStyles } from '@material-ui/core/styles'; -import TextField from '@material-ui/core/TextField'; -import { Operators, NumericOptions, DateOptions } from './Data'; import Results from '../results/Results'; import SearchMap from '../maps/SearchMap'; -import { - createBasicQueriesBySource, - changeNameToLabel, - SearchField, - removeNullFields, - getSections, - getFieldsBySection, - updateArrayElement, - removeArrayElement, - updateSearchFieldValues, - createAdvancedQueriesBySource, -} from '../../Utils.js'; +import { removeNullFields } from '../../Utils.js'; import { fetchPublicFields, fetchUserPolicyFields, fetchSources, - searchQuery, - getQueryCount, } from '../../actions/source'; -import { addUserHistory, fetchUserHistory } from '../../actions/user'; +import { fetchUserHistory } from '../../actions/user'; import { useTranslation } from 'react-i18next'; +import AdvancedSearch from './AdvancedSearch/AdvancedSearch'; +import BasicSearch from './BasicSearch/BasicSearch'; +import styles from './styles'; -const useStyles = makeStyles((theme) => ({ - container: { - display: 'flex', - flexWrap: 'wrap', - }, - textField: { - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - width: 240, - }, -})); - -const fieldValuesToString = (field) => { - let strValues = ''; - switch (field.type) { - case 'Numeric': - field.values.forEach((element) => { - switch (element.option) { - case 'between': - strValues = `${strValues} ${element.value1} <= ${field.name} <= ${element.value2} or `; - break; - default: - strValues = `${strValues} ${field.name} ${element.option} ${element.value1} or `; - } - }); - if (strValues.endsWith('or ')) - strValues = strValues.substring(0, strValues.length - 4); - break; - case 'Date': - field.values.forEach((element) => { - switch (element.option) { - case 'between': - strValues = `${strValues} ${element.startDate} <= ${field.name} <= ${element.endDate} or `; - break; - default: - strValues = `${strValues} ${field.name} ${element.option} ${element.startDate} or `; - } - }); - if (strValues.endsWith(' or ')) - strValues = strValues.substring(0, strValues.length - 4); - break; - case 'List': - strValues = `${strValues} ${field.name} = `; - field.values.forEach((element) => { - strValues = `${strValues} ${element.label}, `; - }); - if (strValues.endsWith(', ')) - strValues = strValues.substring(0, strValues.length - 2); - break; - - //type : text - default: - strValues = `${strValues} ${field.name} = ${field.values}`; - } - return strValues; -}; - -const updateSources = ( - searchFields, - sources, - setSelectedSources, - setAvailableSources -) => { - let updatedSources = []; - let availableSources = []; - let noPrivateField = true; - //search for policy fields to filter sources - searchFields.forEach((field) => { - if (field.isValidated) { - //if sources haven't already been filtered - if (noPrivateField && !updatedSources.length) { - availableSources = sources; - } else { - availableSources = updatedSources; - } - updatedSources = []; - field.sources.forEach((sourceId) => { - noPrivateField = false; - const source = availableSources.find((src) => src.id === sourceId); - if (source && !updatedSources.includes(source)) updatedSources.push(source); - }); - } - }); - setSelectedSources(updatedSources); - if (noPrivateField && !updatedSources.length) { - setAvailableSources(sources); - } else { - setAvailableSources(updatedSources); - } -}; - -const fetchHistory = (setUserHistory) => { +export const fetchHistory = (setUserHistory) => { fetchUserHistory(sessionStorage.getItem('user_id')).then((result) => { if (result[0] && result[0].ui_structure) { result.forEach((item) => { @@ -167,1362 +37,13 @@ const fetchHistory = (setUserHistory) => { }); }; -const addHistory = ( - kcID, - search, - searchName, - searchFields, - searchDescription, - setUserHistory -) => { - addUserHistory( - sessionStorage.getItem('user_id'), - search, - searchName, - searchFields, - searchDescription - ).then(() => { - fetchHistory(setUserHistory); - }); -}; - -const updateSearch = (setSearch, searchFields, selectedOperatorId, setSearchCount) => { - let searchText = ''; - searchFields.forEach((field) => { - if (field.isValidated) { - searchText = - searchText + - `{${fieldValuesToString(field)} } ${Operators[selectedOperatorId].value.toUpperCase()} `; - } - }); - if (searchText.endsWith(' AND ')) { - searchText = searchText.substring(0, searchText.length - 5); - } else if (searchText.endsWith(' OR ')) { - searchText = searchText.substring(0, searchText.length - 4); - } - setSearchCount(); - setSearch(searchText); -}; - -const HistorySelect = ( - sources, - setAvailableSources, - setSelectedSources, - setSearch, - searchFields, - selectedOperatorId, - userHistory, - setUserHistory, - setSearchFields, - setSearchCount, - setFieldCount, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError -) => { - if (Object.keys(userHistory).length !== 0) { - const onHistoryChange = (selectedSavedSearch) => { - setHistorySelectError(undefined); - if (!!selectedSavedSearch[0].query) { - setSelectedSavedSearch(selectedSavedSearch); - setSearch(selectedSavedSearch[0].query); - setSearchCount(); - setFieldCount([]); - } - if (!!selectedSavedSearch[0].ui_structure) { - updateSources( - selectedSavedSearch[0].ui_structure, - sources, - setSelectedSources, - setAvailableSources - ); - setSearchFields(selectedSavedSearch[0].ui_structure); - } - }; - - const onHistorySearchChange = (value, hasMatchingOptions) => { - setHistorySelectError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - - return ( - <> - <EuiFormRow - error={historySelectError} - isInvalid={historySelectError !== undefined} - > - <EuiComboBox - placeholder="Load a previous search" - singleSelection={{ asPlainText: true }} - options={userHistory} - selectedOptions={selectedSavedSearch} - onChange={onHistoryChange} - onSearchChange={onHistorySearchChange} - /> - </EuiFormRow> - </> - ); - } -}; - -const SearchBar = ( - isLoading, - setIsLoading, - search, - setSearch, - searchResults, - setSearchResults, - searchFields, - setSearchFields, - searchName, - setSearchName, - searchDescription, - setSearchDescription, - readOnlyQuery, - setReadOnlyQuery, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources, - standardFields, - sources, - setSelectedTabNumber, - searchCount, - setSearchCount, - setFieldCount, - isReadOnlyModalOpen, - setIsReadOnlyModalOpen, - isSaveSearchModalOpen, - setIsSaveSearchModalOpen, - userHistory, - setUserHistory, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError, - selectedOperatorId, - createEditableQueryToast -) => { - // const closeReadOnlyModal = () => setIsReadOnlyModalOpen(false) - - /* const switchReadOnly = (readOnlyQuery, isReadOnlyModalOpen) => { - if (readOnlyQuery) { - setIsReadOnlyModalOpen(true) - } else { - setReadOnlyQuery(true) - } */ - /* if (!localStorage.getItem("InSylvaReadOnlySearch") && readOnlyQuery) { - setIsReadOnlyModalOpen(!isReadOnlyModalOpen) - } */ - // } - - /* let readOnlyModal; - - if (isReadOnlyModalOpen) { - readOnlyModal = ( - <EuiOverlayMask> - <EuiConfirmModal - title="Allow query editing" - onCancel={() => closeReadOnlyModal()} - onConfirm={() => { - setReadOnlyQuery(!readOnlyQuery) - closeReadOnlyModal() - }} - cancelButtonText="No" - confirmButtonText="Yes" - buttonColor="danger" - defaultFocusedButton="confirm"> - <p>Be aware that manually editing the query can spoil search results.</p> - <p>The syntax needs to be respected :</p> - <ul>Fields and their values must be given between brackets : { }</ul> - <ul>Check eventual typing mistakes</ul> - <ul>Make sure every opened bracket is properly closed</ul> - <p>Are you sure you want to do this?</p> - </EuiConfirmModal> - </EuiOverlayMask> - ) - }*/ - - const closeSaveSearchModal = () => setIsSaveSearchModalOpen(false); - - let saveSearchModal; - - if (isSaveSearchModalOpen) { - saveSearchModal = ( - <EuiOverlayMask> - <EuiModal onClose={closeSaveSearchModal} initialFocus="[name=searchName]"> - <EuiModalHeader> - <EuiModalHeaderTitle>Save search</EuiModalHeaderTitle> - </EuiModalHeader> - - <EuiModalBody> - <EuiForm> - <EuiFormRow label="Search name"> - <EuiFieldText - name="searchName" - value={searchName} - onChange={(e) => { - setSearchName(e.target.value); - }} - /> - </EuiFormRow> - <EuiFormRow label="Description (optional)"> - <EuiTextArea - value={searchDescription} - onChange={(e) => setSearchDescription(e.target.value)} - placeholder="Search description..." - fullWidth - compressed - /> - </EuiFormRow> - </EuiForm> - </EuiModalBody> - - <EuiModalFooter> - <EuiButtonEmpty - onClick={() => { - closeSaveSearchModal(); - }} - > - Cancel - </EuiButtonEmpty> - <EuiButton - onClick={() => { - if (!!searchName) { - addHistory( - sessionStorage.getItem('user_id'), - search, - searchName, - searchFields, - searchDescription, - setUserHistory - ); - setSearchName(''); - setSearchDescription(''); - closeSaveSearchModal(); - } - }} - fill - > - Save - </EuiButton> - </EuiModalFooter> - </EuiModal> - </EuiOverlayMask> - ); - } - - return ( - <> - {/*!readOnlyQuery ? - <> - <EuiCallOut title="Proceed with caution!" color="warning" iconType="alert"> - <p>Be aware that manually editing the query can spoil search results. The syntax must be respected :</p> - <ul>Fields and their values should be put between brackets : { } - Make sure every opened bracket is properly closed</ul> - <ul>"AND" and "OR" should be capitalized between different fields conditions and lowercased within a field expression</ul> - <ul>Make sure to check eventual typing mistakes</ul> - - </EuiCallOut> - <EuiSpacer size="s" /> - </> - : <></> - */} - <EuiFlexGroup> - <EuiFlexItem> - <EuiTextArea - readOnly={readOnlyQuery} - value={search} - onChange={(e) => setSearch(e.target.value)} - placeholder="Add fields..." - fullWidth - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - size="s" - fill - onClick={() => { - if (search.trim()) { - setIsLoading(true); - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - searchQuery(queriesWithIndices).then((result) => { - // sessionStorage.setItem("searchResults", JSON.stringify(result)) - setSearchResults(result); - setSelectedTabNumber(1); - setIsLoading(false); - }); - } - }} - > - Search - </EuiButton> - <EuiSpacer size="s" /> - {isNaN(searchCount) ? ( - <></> - ) : ( - <> - <EuiTextColor - color="secondary" - style={{ display: 'flex', justifyContent: 'center' }} - > - {searchCount} {searchCount === 1 ? 'result' : 'results'} - </EuiTextColor> - <EuiSpacer size="s" /> - </> - )} - <EuiButton - size="s" - onClick={() => { - if (!!search) { - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) setSearchCount(result); - }); - } - }} - > - Count results - </EuiButton> - <EuiSpacer size="s" /> - <EuiButton - size="s" - onClick={() => { - setIsSaveSearchModalOpen(true); - }} - > - Save search - </EuiButton> - {saveSearchModal} - <EuiSpacer size="s" /> - <EuiSwitch - compressed - label={'Editable'} - checked={!readOnlyQuery} - onChange={() => { - // switchReadOnly(readOnlyQuery, isReadOnlyModalOpen) - setReadOnlyQuery(!readOnlyQuery); - if (readOnlyQuery) { - createEditableQueryToast(); - } - }} - /> - {/* readOnlyModal */} - </EuiFlexItem> - </EuiFlexGroup> - {isLoading && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress postion="fixed" size="l" color="accent" /> - </EuiFlexItem> - </EuiFlexGroup> - )} - <EuiSpacer size="s" /> - <EuiFlexGroup> - <EuiFlexItem> - {HistorySelect( - sources, - setAvailableSources, - setSelectedSources, - setSearch, - searchFields, - selectedOperatorId, - userHistory, - setUserHistory, - setSearchFields, - setSearchCount, - setFieldCount, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError - )} - </EuiFlexItem> - </EuiFlexGroup> - </> - ); -}; - -const PopoverSelect = ( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - fieldCount, - setFieldCount, - selectedSources, - setSelectedSources -) => { - const handleAddfield = () => { - if (!!selectedField[0]) { - const field = standardFields.find( - (item) => - item.field_name.replace(/_|\./g, ' ') === - selectedSection[0].label + ' ' + selectedField[0].label - ); - switch (field.field_type) { - case 'Text': - setSearchFields([ - ...searchFields, - new SearchField(field.field_name, field.field_type, '', false, field.sources), - ]); - break; - case 'List': - setSearchFields([ - ...searchFields, - new SearchField(field.field_name, field.field_type, [], false, field.sources), - ]); - break; - default: - setSearchFields([ - ...searchFields, - new SearchField( - field.field_name, - field.field_type, - [{}], - false, - field.sources - ), - ]); - } - } - }; - - const selectField = () => { - const renderOption = (option, searchValue, contentClassName) => { - const { label, color } = option; - return <EuiHealth color={color}>{label}</EuiHealth>; - }; - if (selectedSection.length) { - return ( - <> - <EuiComboBox - placeholder="Select a field" - singleSelection={{ asPlainText: true }} - options={getFieldsBySection(standardFields, selectedSection[0])} - selectedOptions={selectedField} - onChange={(selected) => setSelectedField(selected)} - isClearable={true} - renderOption={renderOption} - /> - <EuiPopoverFooter> - <EuiButton - size="s" - onClick={() => { - handleAddfield(); - setIsPopoverSelectOpen(false); - setSelectedSection([]); - setSelectedField([]); - }} - > - Add this field - </EuiButton> - </EuiPopoverFooter> - </> - ); - } - }; - - return ( - <EuiPopover - panelPaddingSize="s" - button={ - <EuiButton - iconType="listAdd" - iconSide="left" - onClick={() => setIsPopoverSelectOpen(!isPopoverSelectOpen)} - > - Add field - </EuiButton> - } - isOpen={isPopoverSelectOpen} - closePopover={() => setIsPopoverSelectOpen(false)} - > - <div style={{ width: 'intrinsic', minWidth: 240 }}> - <EuiPopoverTitle>Select a field</EuiPopoverTitle> - <EuiComboBox - placeholder="Select a section" - singleSelection={{ asPlainText: true }} - options={getSections(standardFields)} - selectedOptions={selectedSection} - onChange={(selected) => { - setSelectedSection(selected); - setSelectedField([]); - }} - isClearable={false} - /> - </div> - {selectField()} - </EuiPopover> - ); -}; - -const PopoverValueContent = ( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - isPopoverValueOpen, - setIsPopoverValueOpen, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources -) => { - const onValueSearchChange = (value, hasMatchingOptions) => { - setValueError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - - const validateFieldValues = () => { - let fieldValues; - if (Array.isArray(searchFields[index].values)) { - fieldValues = []; - searchFields[index].values.forEach((value) => { - if (!!value) { - fieldValues.push(value); - } - }); - } else { - fieldValues = searchFields[index].values; - } - - const updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - fieldValues, - true, - searchFields[index].sources - ) - ); - setSearchFields(updatedSearchFields); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - setFieldCount(updateArrayElement(fieldCount, index)); - if (searchFields[index].sources.length) { - const filteredSources = []; - searchFields[index].sources.forEach((sourceId) => { - let source; - if (selectedSources.length) { - source = selectedSources.find((src) => src.id === sourceId); - } else { - source = availableSources.find((src) => src.id === sourceId); - } - if (source) { - filteredSources.push(source); - } - }); - setAvailableSources(filteredSources); - setSelectedSources(filteredSources); - createPolicyToast(); - } - }; - - const invalidateFieldValues = () => { - const updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - searchFields[index].values, - false, - searchFields[index].sources - ) - ); - setSearchFields(updatedSearchFields); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - }; - - const ValuePopoverFooter = (i) => { - if (i === searchFields[index].values.length - 1) { - return ( - <EuiPopoverFooter> - <EuiButton - size="s" - onClick={() => { - setSearchFields( - updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - [...searchFields[index].values, {}], - false, - searchFields[index].sources - ) - ) - ); - }} - > - Add value - </EuiButton> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)); - }} - > - Validate - </EuiButton> - </EuiPopoverFooter> - ); - } - }; - - const addFieldValue = (i, selectedOption) => { - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { option: selectedOption }) - ) - ); - }; - - const getListFieldValues = () => { - const listFieldValues = []; - standardFields - .find((item) => item.field_name === searchFields[index].name) - .values.split(', ') - .sort() - .forEach((element) => { - listFieldValues.push({ label: element }); - }); - return listFieldValues; - }; - - switch (searchFields[index].type) { - case 'Text': - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={'Type values'} - value={searchFields[index].values} - onChange={(e) => - setSearchFields( - updateSearchFieldValues(searchFields, index, e.target.value) - ) - } - /> - </EuiFlexItem> - <EuiPopoverFooter> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, false) - ); - }} - > - Validate - </EuiButton> - </EuiPopoverFooter> - </> - ); - case 'List': - return ( - <> - <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> - <EuiComboBox - placeholder={'Select values'} - options={getListFieldValues()} - selectedOptions={searchFields[index].values} - onChange={(selectedOptions) => { - setValueError(undefined); - setSearchFields( - updateSearchFieldValues(searchFields, index, selectedOptions) - ); - }} - onSearchChange={onValueSearchChange} - /> - </EuiFormRow> - <EuiPopoverFooter> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, false) - ); - }} - > - Validate - </EuiButton> - </EuiPopoverFooter> - </> - ); - - case 'Numeric': - const NumericValues = (i) => { - if (!!searchFields[index].values[i].option) { - switch (searchFields[index].values[i].option) { - case 'between': - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={'1st value'} - 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={'2nd value'} - 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={'Type value'} - 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> - ))} - </> - ); - - 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="between" - 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="and" - 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> - ))} - </> - ); - default: - } -}; - -const PopoverValueButton = ( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources -) => { - return ( - <EuiPopover - panelPaddingSize="s" - button={ - <EuiButtonIcon - size="s" - color="primary" - onClick={() => - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, !isPopoverValueOpen[index]) - ) - } - iconType="documentEdit" - title="Give field values" - /> - } - isOpen={isPopoverValueOpen[index]} - closePopover={() => - setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)) - } - > - {/*<div style={{ width: 240 }}> - <EuiButtonIcon - size="s" - style={{ float: 'right' }} - color="danger" - onClick={() => setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false))} - iconType="crossInACircleFilled" - title="Close popover" - /> - </div>*/} - <div style={{ width: 240 }}> - {PopoverValueContent( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - isPopoverValueOpen, - setIsPopoverValueOpen, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources - )} - </div> - </EuiPopover> - ); -}; - -const FieldsPanel = ( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - selectedOperatorId, - setSelectedOperatorId, - fieldCount, - setFieldCount, - availableSources, - setAvailableSources, - selectedSources, - setSelectedSources, - sources, - datePickerStyles, - createPolicyToast -) => { - const countFieldValues = (field, index) => { - const fieldStr = `{${fieldValuesToString(field)}}`; - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - fieldStr, - selectedSources, - availableSources - ); - getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) - setFieldCount(updateArrayElement(fieldCount, index, result)); - }); - }; - - const handleRemoveField = (index) => { - const updatedSearchFields = removeArrayElement(searchFields, index); - setSearchFields(updatedSearchFields); - updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - }; - - const handleClearValues = (index) => { - let updatedSearchFields = []; - switch (searchFields[index].type) { - case 'Text': - updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - '', - false, - searchFields[index].sources - ) - ); - break; - case 'List': - updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - [], - false, - searchFields[index].sources - ) - ); - break; - default: - updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - [{}], - false, - searchFields[index].sources - ) - ); - } - setSearchFields(updatedSearchFields); - updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); - setFieldCount(updateArrayElement(fieldCount, index)); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - }; - - if (standardFields === []) { - return <h2>Loading user fields...</h2>; - } - - return ( - <> - <EuiTitle size="xs"> - <h2>Field search</h2> - </EuiTitle> - <EuiPanel paddingSize="m"> - <EuiFlexGroup direction="column"> - {searchFields.map((field, index) => ( - <EuiPanel key={'field' + index} paddingSize="s"> - <EuiFlexItem grow={false}> - <EuiFlexGroup direction="row" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiButtonIcon - size="s" - color="danger" - onClick={() => handleRemoveField(index)} - iconType="indexClose" - title="Remove field" - /> - </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"> - {fieldCount[index]}{' '} - {fieldCount[index] === 1 ? 'result' : 'results'} - </EuiTextColor> - </> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {field.isValidated ? ( - <> - <EuiButtonIcon - size="s" - onClick={() => countFieldValues(field, index)} - iconType="number" - title="Count results" - /> - </> - ) : ( - <></> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {field.isValidated ? ( - <> - <EuiButtonIcon - size="s" - color="danger" - onClick={() => handleClearValues(index)} - iconType="trash" - title="Clear values" - /> - </> - ) : ( - <></> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {PopoverValueButton( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources - )} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiPanel> - ))} - </EuiFlexGroup> - <EuiSpacer size="l" /> - {PopoverSelect( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - fieldCount, - setFieldCount, - selectedSources, - setSelectedSources - )} - </EuiPanel> - <EuiSpacer size="s" /> - <EuiRadioGroup - options={Operators} - idSelected={selectedOperatorId} - onChange={(id) => { - setSelectedOperatorId(id); - updateSearch(setSearch, searchFields, id, setSearchCount); - }} - name="operators group" - legend={{ - children: <span>Search option</span>, - }} - /> - </> - ); -}; - -const SourceSelect = ( - availableSources, - selectedSources, - setSelectedSources, - sourceSelectError, - setSourceSelectError -) => { - if (Object.keys(availableSources).length !== 0) { - availableSources.forEach((source) => { - if (source.name) { - source = changeNameToLabel(source); - } - }); - - const onSourceChange = (selectedOptions) => { - setSourceSelectError(undefined); - setSelectedSources(selectedOptions); - }; - - const onSourceSearchChange = (value, hasMatchingOptions) => { - setSourceSelectError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - return ( - <> - <EuiTitle size="xs"> - <h2>Partner sources</h2> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiFlexItem> - <EuiFormRow - error={sourceSelectError} - isInvalid={sourceSelectError !== undefined} - > - <EuiComboBox - placeholder="By default, all sources are selected" - options={availableSources} - selectedOptions={selectedSources} - onChange={onSourceChange} - onSearchChange={onSourceSearchChange} - /> - </EuiFormRow> - </EuiFlexItem> - </> - ); - } else { - return ( - <p> - <EuiIcon type="alert" color="danger" /> No source available ! - </p> - ); - } -}; - const Search = () => { const { t } = useTranslation('search'); - const datePickerStyles = useStyles(); + const datePickerStyles = styles(); const [isLoading, setIsLoading] = useState(false); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [userHistory, setUserHistory] = useState({}); - const [advancedSearch, setAdvancedSearch] = useState(false); + const [isAdvancedSearch, setIsAdvancedSearch] = useState(false); const [readOnlyQuery, setReadOnlyQuery] = useState(true); const [selectedField, setSelectedField] = useState([]); const [selectedSection, setSelectedSection] = useState([]); @@ -1578,9 +99,6 @@ const Search = () => { setStandardFields(removeNullFields(userFields)); } ); - - // policyField => { - // policyField.forEach( }); fetchSources(sessionStorage.getItem('user_id')).then((result) => { setSources(result); @@ -1589,230 +107,84 @@ const Search = () => { fetchHistory(setUserHistory); }, []); - const createPolicyToast = () => { - const toast = { - title: 'Policy field selected', - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 15000, - text: ( - <> - <p>You selected a private field.</p> - <p> - Access to this field was granted for specific sources, which means that your - search will be restricted to those. - </p> - <p>Please check the sources list before searching.</p> - </> - ), - }; - setNotificationToasts(notificationToasts.concat(toast)); - }; - - const createEditableQueryToast = () => { - const toast = { - title: 'Proceed with caution', - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 15000, - text: ( - <> - <p> - Be aware that manually editing the query can spoil search results. The syntax - must be respected : - </p> - <ul> - Fields and their values should be put between brackets : { } - Make - sure every opened bracket is properly closed - </ul> - <ul> - "AND" and "OR" should be capitalized between different fields conditions and - lowercased within a field expression - </ul> - <ul>Make sure to check eventual typing mistakes</ul> - </> - ), - }; - setNotificationToasts(notificationToasts.concat(toast)); - }; - - const removeToast = (removedToast) => { - setNotificationToasts( - notificationToasts.filter((toast) => toast.id !== removedToast.id) - ); - }; - - const onFormSubmit = () => { - setIsLoading(true); - const queriesWithIndices = createBasicQueriesBySource( - standardFields, - basicSearch, - selectedSources, - availableSources - ); - searchQuery(queriesWithIndices).then((result) => { - setSearchResults(result); - setSelectedTabNumber(1); - setIsLoading(false); - }); - }; - const tabsContent = [ { id: 'tab1', name: 'Compose search', content: ( <> - {advancedSearch ? ( - <> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> - <EuiButtonEmpty - onClick={() => { - setAdvancedSearch(!advancedSearch); - }} - > - Switch to basic search - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - {SearchBar( - isLoading, - setIsLoading, - search, - setSearch, - searchResults, - setSearchResults, - searchFields, - setSearchFields, - searchName, - setSearchName, - searchDescription, - setSearchDescription, - readOnlyQuery, - setReadOnlyQuery, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources, - standardFields, - sources, - setSelectedTabNumber, - searchCount, - setSearchCount, - setFieldCount, - isReadOnlyModalOpen, - setIsReadOnlyModalOpen, - isSaveSearchModalOpen, - setIsSaveSearchModalOpen, - userHistory, - setUserHistory, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError, - selectedOperatorId, - createEditableQueryToast - )} - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - {FieldsPanel( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - selectedOperatorId, - setSelectedOperatorId, - fieldCount, - setFieldCount, - availableSources, - setAvailableSources, - selectedSources, - setSelectedSources, - sources, - datePickerStyles, - createPolicyToast - )} - <EuiSpacer size="s" /> - {SourceSelect( - availableSources, - selectedSources, - setSelectedSources, - sourceSelectError, - setSourceSelectError - )} - </EuiFlexItem> - </EuiFlexGroup> - <EuiGlobalToastList - toasts={notificationToasts} - dismissToast={removeToast} - toastLifeTimeMs={2500} - /> - </> + {isAdvancedSearch ? ( + <AdvancedSearch + isLoading={isLoading} + setIsLoading={setIsLoading} + search={search} + setSearch={setSearch} + searchResults={searchResults} + setSearchResults={setSearchResults} + searchFields={searchFields} + setSearchFields={setSearchFields} + searchName={searchName} + setSearchName={setSearchName} + searchDescription={searchDescription} + setSearchDescription={setSearchDescription} + readOnlyQuery={readOnlyQuery} + setReadOnlyQuery={setReadOnlyQuery} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + standardFields={standardFields} + setStandardFields={setStandardFields} + sources={sources} + setSelectedTabNumber={setSelectedTabNumber} + searchCount={searchCount} + setSearchCount={setSearchCount} + setFieldCount={setFieldCount} + isReadOnlyModalOpen={isReadOnlyModalOpen} + setIsReadOnlyModalOpen={setIsReadOnlyModalOpen} + isSaveSearchModalOpen={isSaveSearchModalOpen} + setIsSaveSearchModalOpen={setIsSaveSearchModalOpen} + userHistory={userHistory} + setUserHistory={setUserHistory} + selectedSavedSearch={selectedSavedSearch} + setSelectedSavedSearch={setSelectedSavedSearch} + historySelectError={historySelectError} + setHistorySelectError={setHistorySelectError} + selectedOperatorId={selectedOperatorId} + notificationToasts={notificationToasts} + setNotificationToasts={setNotificationToasts} + setIsAdvancedSearch={setIsAdvancedSearch} + isAdvancedSearch={isAdvancedSearch} + selectedField={selectedField} + selectedSection={selectedSection} + setSelectedField={setSelectedField} + setSelectedSection={setSelectedSection} + isPopoverSelectOpen={isPopoverSelectOpen} + setIsPopoverSelectOpen={setIsPopoverSelectOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + isPopoverValueOpen={isPopoverValueOpen} + valueError={valueError} + setValueError={setValueError} + setSelectedOperatorId={setSelectedOperatorId} + fieldCount={fieldCount} + sourceSelectError={sourceSelectError} + datePickerStyles={datePickerStyles} + setSourceSelectError={setSourceSelectError} + /> ) : ( - <> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> - <EuiButtonEmpty - onClick={() => { - setAdvancedSearch(!advancedSearch); - }} - > - Switch to advanced search - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - <form onSubmit={onFormSubmit}> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFieldSearch - value={basicSearch} - onChange={(e) => setBasicSearch(e.target.value)} - placeholder="Search..." - fullWidth - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton type="submit" fill isDisabled={advancedSearch}> - Search - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </form> - {isLoading && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress postion="fixed" size="l" color="accent" /> - </EuiFlexItem> - </EuiFlexGroup> - )} - </EuiFlexItem> - </EuiFlexGroup> - </> + <BasicSearch + isAdvancedSearch={isAdvancedSearch} + setIsAdvancedSearch={setIsAdvancedSearch} + standardFields={standardFields} + availableSources={availableSources} + selectedSources={selectedSources} + basicSearch={basicSearch} + setBasicSearch={setBasicSearch} + isLoading={isLoading} + setIsLoading={setIsLoading} + setSearchResults={setSearchResults} + setSelectedTabNumber={setSelectedTabNumber} + /> )} </> ), @@ -1822,7 +194,13 @@ const Search = () => { name: 'Results', content: ( <EuiFlexGroup> - <EuiFlexItem>{Results(searchResults, search, basicSearch)}</EuiFlexItem> + <EuiFlexItem> + <Results + searchResults={searchResults} + search={search} + basicSearch={basicSearch} + /> + </EuiFlexItem> </EuiFlexGroup> ), }, @@ -1833,7 +211,6 @@ const Search = () => { <EuiFlexGroup> <EuiFlexItem> <EuiSpacer size="l" /> - {/*<a href="https://agroenvgeo.data.inra.fr/mapfishapp/"><img src={map} width="460" height="400" alt='Map' /></a>*/} <SearchMap searchResults={searchResults} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/src/pages/search/styles.js b/src/pages/search/styles.js new file mode 100644 index 0000000..66022dc --- /dev/null +++ b/src/pages/search/styles.js @@ -0,0 +1,15 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const style = makeStyles((theme) => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + textField: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + width: 240, + }, +})); + +export default style; -- GitLab From cce4543ec8b123b853eb1bcb2936d972a5121f3e Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 7 May 2024 11:42:46 +0200 Subject: [PATCH 11/17] [BasicSearch.js]: added full translations --- public/locales/en/search.json | 12 +++++++++++- public/locales/fr/search.json | 10 +++++++++- src/pages/search/BasicSearch/BasicSearch.js | 9 ++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/public/locales/en/search.json b/public/locales/en/search.json index 26c286e..a7edd65 100644 --- a/public/locales/en/search.json +++ b/public/locales/en/search.json @@ -1,3 +1,13 @@ { - "pageTitle": "In-Sylva Metadata Search Platform" + "pageTitle": "In-Sylva Metadata Search Platform", + "basicSearch": { + "switchSearchMode": "Switch to advanced search", + "searchInputPlaceholder": "Search...", + "sendSearchButton": "Search" + }, + "advancedSearch": { + "searchHistory": { + "placeholder": "Load a previous search" + } + } } diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json index 866c88a..812460c 100644 --- a/public/locales/fr/search.json +++ b/public/locales/fr/search.json @@ -1,3 +1,11 @@ { - "pageTitle": "Plateforme de recherche de métadonnées In-Sylva" + "pageTitle": "Plateforme de recherche de métadonnées In-Sylva", + "basicSearch": { + "switchSearchMode": "Passer en recherche avancée", + "searchInputPlaceholder": "Chercher...", + "sendSearchButton": "Lancer la recherche" + }, + "searchHistory": { + "placeholder": "Charger une recherche précédente" + } } diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js index 20e7296..fc9710d 100644 --- a/src/pages/search/BasicSearch/BasicSearch.js +++ b/src/pages/search/BasicSearch/BasicSearch.js @@ -10,6 +10,7 @@ import { } from '@elastic/eui'; import { createBasicQueriesBySource } from '../../../Utils'; import { searchQuery } from '../../../actions/source'; +import { useTranslation } from 'react-i18next'; const BasicSearch = ({ standardFields, @@ -24,6 +25,8 @@ const BasicSearch = ({ setSearchResults, setSelectedTabNumber, }) => { + const { t } = useTranslation('search'); + const onFormSubmit = () => { setIsLoading(true); const queriesWithIndices = createBasicQueriesBySource( @@ -49,7 +52,7 @@ const BasicSearch = ({ setIsAdvancedSearch(!isAdvancedSearch); }} > - Switch to advanced search + {t('basicSearch.switchSearchMode')} </EuiButtonEmpty> </EuiFlexItem> </EuiFlexGroup> @@ -62,13 +65,13 @@ const BasicSearch = ({ <EuiFieldSearch value={basicSearch} onChange={(e) => setBasicSearch(e.target.value)} - placeholder="Search..." + placeholder={t('basicSearch.searchInputPlaceholder')} fullWidth /> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton type="submit" fill isDisabled={isAdvancedSearch}> - Search + {t('basicSearch.sendSearchButton')} </EuiButton> </EuiFlexItem> </EuiFlexGroup> -- GitLab From 1d755870424373d5206fe3d1ce75002da35ac183 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 7 May 2024 12:08:20 +0200 Subject: [PATCH 12/17] [Search.js]: added full translations --- public/locales/en/search.json | 5 +++++ public/locales/fr/search.json | 5 +++++ src/pages/search/Search.js | 6 +++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/public/locales/en/search.json b/public/locales/en/search.json index a7edd65..5af75fb 100644 --- a/public/locales/en/search.json +++ b/public/locales/en/search.json @@ -1,5 +1,10 @@ { "pageTitle": "In-Sylva Metadata Search Platform", + "tabs": { + "composeSearch": "Compose search", + "results": "Results", + "map": "Map" + }, "basicSearch": { "switchSearchMode": "Switch to advanced search", "searchInputPlaceholder": "Search...", diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json index 812460c..3c50743 100644 --- a/public/locales/fr/search.json +++ b/public/locales/fr/search.json @@ -1,5 +1,10 @@ { "pageTitle": "Plateforme de recherche de métadonnées In-Sylva", + "tabs": { + "composeSearch": "Composer une recherche", + "results": "Résultats", + "map": "Carte" + }, "basicSearch": { "switchSearchMode": "Passer en recherche avancée", "searchInputPlaceholder": "Chercher...", diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 16d3043..256ce8d 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -110,7 +110,7 @@ const Search = () => { const tabsContent = [ { id: 'tab1', - name: 'Compose search', + name: t('tabs.composeSearch'), content: ( <> {isAdvancedSearch ? ( @@ -191,7 +191,7 @@ const Search = () => { }, { id: 'tab3', - name: 'Results', + name: t('tabs.results'), content: ( <EuiFlexGroup> <EuiFlexItem> @@ -206,7 +206,7 @@ const Search = () => { }, { id: 'tab2', - name: 'Map', + name: t('tabs.map'), content: ( <EuiFlexGroup> <EuiFlexItem> -- GitLab From 603541fbdaa2da73fb1c5e789b5bda3e56f46483 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Wed, 15 May 2024 17:14:38 +0200 Subject: [PATCH 13/17] [AdvancedSearch.js]: added full translation for component --- public/locales/en/common.json | 4 +- public/locales/en/search.json | 66 +- public/locales/fr/common.json | 4 +- public/locales/fr/search.json | 70 +- .../search/AdvancedSearch/AdvancedSearch.js | 748 +++++++++--------- src/pages/search/BasicSearch/BasicSearch.js | 13 +- src/pages/search/Data.js | 4 +- src/pages/search/Search.js | 28 - 8 files changed, 527 insertions(+), 410 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 48a5dcf..506293c 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -6,6 +6,8 @@ "inSylvaLogoAlt": "In-Sylva logo", "validationActions": { "cancel": "Cancel", - "send": "Send" + "send": "Send", + "save": "Save", + "validate": "Validate" } } diff --git a/public/locales/en/search.json b/public/locales/en/search.json index 5af75fb..cebc9eb 100644 --- a/public/locales/en/search.json +++ b/public/locales/en/search.json @@ -5,14 +5,74 @@ "results": "Results", "map": "Map" }, + "sendSearchButton": "Search", "basicSearch": { "switchSearchMode": "Switch to advanced search", - "searchInputPlaceholder": "Search...", - "sendSearchButton": "Search" + "searchInputPlaceholder": "Search..." }, "advancedSearch": { + "switchSearchMode": "Switch to basic search", + "textQueryPlaceholder": "Add fields...", + "countResultsButton": "Count results", + "resultsCount_one": "{{count}} result", + "resultsCount_other": "{{count}} results", + "editableSearchButton": "Editable", + "errorInvalidOption": "\"{{value}}\" is not a valid option.", + "fields": { + "title": "Field search", + "loadingFields": "Loading fields...", + "removeFieldButton": "Remove field", + "clearValues": "Clear values", + "addFieldPopover": { + "openPopoverButton": "Add field", + "title": "Select a field", + "button": "Add this field", + "selectSection": "Select a section" + }, + "fieldContentPopover": { + "addFieldValues": "Add field values", + "addValue": "Add value", + "firstValue": "1st value", + "secondValue": "2nd value", + "inputTextValue": "Type value", + "betweenDate": "between", + "andDate": "and", + "selectValues": "Select values" + } + }, "searchHistory": { - "placeholder": "Load a previous search" + "placeholder": "Load a previous request", + "saveSearch": "Save search", + "addSavedSearchName": "Search name", + "addSavedSearchDescription": "Description (optional)", + "addSavedSearchDescriptionPlaceholder": "Search description..." + }, + "searchOptions": { + "title": "Search option", + "matchAll": "Match all criterias", + "matchAtLeastOne": "Match at least one criteria" + }, + "partnerSources": { + "title": "Partner sources", + "allSourcesSelected": "By default, all sources are selected", + "noSourceAvailable": "No source available." + }, + "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." + ] + }, + "editableQueryToast": { + "title": "Proceed with caution", + "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" + } } } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 74be05d..8e9085f 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -6,6 +6,8 @@ "inSylvaLogoAlt": "Logo In-Sylva", "validationActions": { "cancel": "Annuler", - "send": "Envoyer" + "send": "Envoyer", + "save": "Sauvegarder", + "validate": "Valider" } } diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json index 3c50743..5f6ab39 100644 --- a/public/locales/fr/search.json +++ b/public/locales/fr/search.json @@ -5,12 +5,74 @@ "results": "Résultats", "map": "Carte" }, + "sendSearchButton": "Lancer la recherche", "basicSearch": { "switchSearchMode": "Passer en recherche avancée", - "searchInputPlaceholder": "Chercher...", - "sendSearchButton": "Lancer la recherche" + "searchInputPlaceholder": "Chercher..." }, - "searchHistory": { - "placeholder": "Charger une recherche précédente" + "advancedSearch": { + "switchSearchMode": "Passer en recherche basique", + "textQueryPlaceholder": "Ajoutez des champs...", + "countResultsButton": "Compter les résultats", + "resultsCount_one": "{{count}} résultat", + "resultsCount_other": "{{count}} résultats", + "editableSearchButton": "Modifiable", + "errorInvalidOption": "\"{{value}}\" n'est pas une option valide.", + "fields": { + "title": "Recherche de champ", + "loadingFields": "Chargement des champs...", + "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" + }, + "fieldContentPopover": { + "addValue": "Ajouter une valeur", + "addFieldValues": "Ajouter des valeurs de champ", + "firstValue": "1ère valeur", + "secondValue": "2ème valeur", + "inputTextValue": "Entrez une valeur", + "betweenDate": "entre", + "andDate": "et", + "selectValues": "Sélectionnez au moins une valeur" + } + }, + "searchHistory": { + "placeholder": "Charger une recherche précédente", + "saveSearch": "Sauvegarder ma recherche", + "addSavedSearchName": "Nom de la recherche", + "addSavedSearchDescription": "Description (optionel)", + "addSavedSearchDescriptionPlaceholder": "Description de la recherche..." + }, + "searchOptions": { + "title": "Option de recherche", + "matchAll": "Répondre à tous les critères", + "matchAtLeastOne": "Répondre à au moins un critère" + }, + "partnerSources": { + "title": "Liste des sources de partenaires", + "allSourcesSelected": "Toutes les sources sont sélectionnées par défaut", + "noSourceAvailable": "Pas de source disponible." + }, + "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." + ] + }, + "editableQueryToast": { + "title": "Procéder avec prudence", + "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" + } + } } } diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js index f8db71a..cc2030a 100644 --- a/src/pages/search/AdvancedSearch/AdvancedSearch.js +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -30,7 +30,7 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { changeNameToLabel, createAdvancedQueriesBySource, @@ -44,8 +44,8 @@ import { import { getQueryCount, searchQuery } from '../../../actions/source'; import { DateOptions, NumericOptions, Operators } from '../Data'; import TextField from '@material-ui/core/TextField'; -import { addUserHistory } from '../../../actions/user'; -import { fetchHistory } from '../Search'; +import { addUserHistory, fetchUserHistory } from '../../../actions/user'; +import { useTranslation } from 'react-i18next'; const updateSources = ( searchFields, @@ -144,6 +144,18 @@ const addHistory = ( }); }; +const fetchHistory = (setUserHistory) => { + fetchUserHistory(sessionStorage.getItem('user_id')).then((result) => { + if (result[0] && result[0].ui_structure) { + result.forEach((item) => { + item.ui_structure = JSON.parse(item.ui_structure); + item.label = `${item.name} - ${new Date(item.createdat).toLocaleString()}`; + }); + } + setUserHistory(result); + }); +}; + const updateSearch = (setSearch, searchFields, selectedOperatorId, setSearchCount) => { let searchText = ''; searchFields.forEach((field) => { @@ -162,59 +174,62 @@ const updateSearch = (setSearch, searchFields, selectedOperatorId, setSearchCoun setSearch(searchText); }; -const HistorySelect = ( +const HistorySelect = ({ sources, setAvailableSources, setSelectedSources, setSearch, - searchFields, - selectedOperatorId, - userHistory, - setUserHistory, setSearchFields, setSearchCount, setFieldCount, + userHistory, + setUserHistory, selectedSavedSearch, setSelectedSavedSearch, - historySelectError, - setHistorySelectError -) => { - if (Object.keys(userHistory).length !== 0) { - const onHistoryChange = (selectedSavedSearch) => { - setHistorySelectError(undefined); - if (!!selectedSavedSearch[0].query) { - setSelectedSavedSearch(selectedSavedSearch); - setSearch(selectedSavedSearch[0].query); - setSearchCount(); - setFieldCount([]); - } - if (!!selectedSavedSearch[0].ui_structure) { - updateSources( - selectedSavedSearch[0].ui_structure, - sources, - setSelectedSources, - setAvailableSources - ); - setSearchFields(selectedSavedSearch[0].ui_structure); - } - }; +}) => { + const [historySelectError, setHistorySelectError] = useState(undefined); + const { t } = useTranslation('search'); - const onHistorySearchChange = (value, hasMatchingOptions) => { - setHistorySelectError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` + useEffect(() => { + fetchHistory(setUserHistory); + }, [setUserHistory]); + + const onHistoryChange = (selectedSavedSearch) => { + setHistorySelectError(undefined); + if (!!selectedSavedSearch[0].query) { + setSelectedSavedSearch(selectedSavedSearch); + setSearch(selectedSavedSearch[0].query); + setSearchCount(); + setFieldCount([]); + } + if (!!selectedSavedSearch[0].ui_structure) { + updateSources( + selectedSavedSearch[0].ui_structure, + sources, + setSelectedSources, + setAvailableSources ); - }; + setSearchFields(selectedSavedSearch[0].ui_structure); + } + }; - return ( - <> + const onHistorySearchChange = (value, hasMatchingOptions) => { + if (value.length === 0 || hasMatchingOptions) { + setHistorySelectError(undefined); + } else { + setHistorySelectError(t('search:advancedSearch.errorInvalidOption', { value })); + } + }; + + return ( + <> + {userHistory && Object.keys(userHistory).length !== 0 && ( <EuiFormRow error={historySelectError} isInvalid={historySelectError !== undefined} > <EuiComboBox - placeholder={'searchHistory.placeholder'} + placeholder={t('search:advancedSearch.searchHistory.placeholder')} singleSelection={{ asPlainText: true }} options={userHistory} selectedOptions={selectedSavedSearch} @@ -222,14 +237,12 @@ const HistorySelect = ( onSearchChange={onHistorySearchChange} /> </EuiFormRow> - </> - ); - } + )} + </> + ); }; const SearchBar = ({ - isLoading, - setIsLoading, search, setSearch, setSearchResults, @@ -253,20 +266,20 @@ const SearchBar = ({ setFieldCount, isSaveSearchModalOpen, setIsSaveSearchModalOpen, - userHistory, - setUserHistory, selectedSavedSearch, setSelectedSavedSearch, - historySelectError, - setHistorySelectError, selectedOperatorId, createEditableQueryToast, }) => { + const { t } = useTranslation(['search', 'common']); + const [userHistory, setUserHistory] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const closeSaveSearchModal = () => { setIsSaveSearchModalOpen(false); }; - const onSendAdvancedSearch = () => { + const onClickAdvancedSearch = () => { if (search.trim()) { setIsLoading(true); const queriesWithIndices = createAdvancedQueriesBySource( @@ -283,19 +296,48 @@ const SearchBar = ({ } }; - let saveSearchModal; + const onClickCountResults = () => { + if (!!search) { + const queriesWithIndices = createAdvancedQueriesBySource( + standardFields, + search, + selectedSources, + availableSources + ); + getQueryCount(queriesWithIndices).then((result) => { + if (result || result === 0) setSearchCount(result); + }); + } + }; + + const onClickSaveSearch = () => { + if (!!searchName) { + addHistory( + sessionStorage.getItem('user_id'), + search, + searchName, + searchFields, + searchDescription, + setUserHistory + ); + setSearchName(''); + setSearchDescription(''); + closeSaveSearchModal(); + } + }; - if (isSaveSearchModalOpen) { - saveSearchModal = ( + const SaveSearchModal = () => { + return ( <EuiOverlayMask> <EuiModal onClose={closeSaveSearchModal} initialFocus="[name=searchName]"> <EuiModalHeader> - <EuiModalHeaderTitle>Save search</EuiModalHeaderTitle> + <EuiModalHeaderTitle> + {t('advancedSearch.searchHistory.saveSearch')} + </EuiModalHeaderTitle> </EuiModalHeader> - <EuiModalBody> <EuiForm> - <EuiFormRow label="Search name"> + <EuiFormRow label={t('advancedSearch.searchHistory.addSavedSearchName')}> <EuiFieldText name="searchName" value={searchName} @@ -304,11 +346,15 @@ const SearchBar = ({ }} /> </EuiFormRow> - <EuiFormRow label="Description (optional)"> + <EuiFormRow + label={t('advancedSearch.searchHistory.addSavedSearchDescription')} + > <EuiTextArea value={searchDescription} onChange={(e) => setSearchDescription(e.target.value)} - placeholder="Search description..." + placeholder={t( + 'advancedSearch.searchHistory.addSavedSearchDescriptionPlaceholder' + )} fullWidth compressed /> @@ -322,33 +368,21 @@ const SearchBar = ({ closeSaveSearchModal(); }} > - Cancel + {t('common:validationActions.cancel')} </EuiButtonEmpty> <EuiButton onClick={() => { - if (!!searchName) { - addHistory( - sessionStorage.getItem('user_id'), - search, - searchName, - searchFields, - searchDescription, - setUserHistory - ); - setSearchName(''); - setSearchDescription(''); - closeSaveSearchModal(); - } + onClickSaveSearch(); }} fill > - Save + {t('common:validationActions.save')} </EuiButton> </EuiModalFooter> </EuiModal> </EuiOverlayMask> ); - } + }; return ( <> @@ -358,7 +392,7 @@ const SearchBar = ({ readOnly={readOnlyQuery} value={search} onChange={(e) => setSearch(e.target.value)} - placeholder="Add fields..." + placeholder={t('search:advancedSearch.textQueryPlaceholder')} fullWidth /> </EuiFlexItem> @@ -367,21 +401,19 @@ const SearchBar = ({ size="s" fill onClick={() => { - onSendAdvancedSearch(); + onClickAdvancedSearch(); }} > - Search + {t('search:sendSearchButton')} </EuiButton> <EuiSpacer size="s" /> - {isNaN(searchCount) ? ( - <></> - ) : ( + {!isNaN(searchCount) && ( <> <EuiTextColor color="secondary" style={{ display: 'flex', justifyContent: 'center' }} > - {searchCount} {searchCount === 1 ? 'result' : 'results'} + {t('search:advancedSearch.resultsCount', { count: searchCount })} </EuiTextColor> <EuiSpacer size="s" /> </> @@ -389,20 +421,10 @@ const SearchBar = ({ <EuiButton size="s" onClick={() => { - if (!!search) { - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) setSearchCount(result); - }); - } + onClickCountResults(); }} > - Count results + {t('search:advancedSearch.countResultsButton')} </EuiButton> <EuiSpacer size="s" /> <EuiButton @@ -411,13 +433,13 @@ const SearchBar = ({ setIsSaveSearchModalOpen(true); }} > - Save search + {t('search:advancedSearch.searchHistory.saveSearch')} </EuiButton> - {saveSearchModal} + {isSaveSearchModalOpen && <SaveSearchModal />} <EuiSpacer size="s" /> <EuiSwitch compressed - label={'Editable'} + label={t('search:advancedSearch.editableSearchButton')} checked={!readOnlyQuery} onChange={() => { setReadOnlyQuery(!readOnlyQuery); @@ -438,32 +460,29 @@ const SearchBar = ({ <EuiSpacer size="s" /> <EuiFlexGroup> <EuiFlexItem> - {HistorySelect( - sources, - setAvailableSources, - setSelectedSources, - setSearch, - searchFields, - selectedOperatorId, - userHistory, - setUserHistory, - setSearchFields, - setSearchCount, - setFieldCount, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError - )} + <HistorySelect + sources={sources} + setAvailableSources={setAvailableSources} + setSelectedSources={setSelectedSources} + setSearch={setSearch} + searchFields={searchFields} + selectedOperatorId={selectedOperatorId} + userHistory={userHistory} + setUserHistory={setUserHistory} + setSearchFields={setSearchFields} + setSearchCount={setSearchCount} + setFieldCount={setFieldCount} + selectedSavedSearch={selectedSavedSearch} + setSelectedSavedSearch={setSelectedSavedSearch} + /> </EuiFlexItem> </EuiFlexGroup> </> ); }; -const PopoverSelect = ( +const PopoverSelect = ({ standardFields, - setStandardFields, searchFields, setSearchFields, selectedField, @@ -471,9 +490,11 @@ const PopoverSelect = ( selectedSection, setSelectedSection, isPopoverSelectOpen, - setIsPopoverSelectOpen -) => { - const handleAddfield = () => { + setIsPopoverSelectOpen, +}) => { + const { t } = useTranslation('search'); + + const handleAddField = () => { if (!!selectedField[0]) { const field = standardFields.find( (item) => @@ -517,7 +538,7 @@ const PopoverSelect = ( return ( <> <EuiComboBox - placeholder="Select a field" + placeholder={t('search:advancedSearch.fields.addFieldPopover.title')} singleSelection={{ asPlainText: true }} options={getFieldsBySection(standardFields, selectedSection[0])} selectedOptions={selectedField} @@ -529,13 +550,13 @@ const PopoverSelect = ( <EuiButton size="s" onClick={() => { - handleAddfield(); + handleAddField(); setIsPopoverSelectOpen(false); setSelectedSection([]); setSelectedField([]); }} > - Add this field + {t('search:advancedSearch.fields.addFieldPopover.button')} </EuiButton> </EuiPopoverFooter> </> @@ -552,16 +573,18 @@ const PopoverSelect = ( iconSide="left" onClick={() => setIsPopoverSelectOpen(!isPopoverSelectOpen)} > - Add field + {t('search:advancedSearch.fields.addFieldPopover.openPopoverButton')} </EuiButton> } isOpen={isPopoverSelectOpen} closePopover={() => setIsPopoverSelectOpen(false)} > <div style={{ width: 'intrinsic', minWidth: 240 }}> - <EuiPopoverTitle>Select a field</EuiPopoverTitle> + <EuiPopoverTitle> + {t('search:advancedSearch.fields.addFieldPopover.title')} + </EuiPopoverTitle> <EuiComboBox - placeholder="Select a section" + placeholder={t('search:advancedSearch.fields.addFieldPopover.selectSection')} singleSelection={{ asPlainText: true }} options={getSections(standardFields)} selectedOptions={selectedSection} @@ -577,15 +600,13 @@ const PopoverSelect = ( ); }; -const PopoverValueContent = ( +const PopoverValueContent = ({ index, standardFields, - setStandardFields, searchFields, setSearchFields, valueError, setValueError, - search, setSearch, setSearchCount, fieldCount, @@ -598,14 +619,16 @@ const PopoverValueContent = ( selectedSources, setSelectedSources, availableSources, - setAvailableSources -) => { + setAvailableSources, +}) => { + const { t } = useTranslation(['search', 'common']); + const onValueSearchChange = (value, hasMatchingOptions) => { - setValueError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); + if (value.length === 0 || hasMatchingOptions) { + setValueError(undefined); + } else { + setValueError(t('search:advancedSearch.errorInvalidOption', { value })); + } }; const validateFieldValues = () => { @@ -692,7 +715,7 @@ const PopoverValueContent = ( ); }} > - Add value + {t('search:advancedSearch.fields.fieldContentPopover.addValue')} </EuiButton> <EuiButton size="s" @@ -702,7 +725,7 @@ const PopoverValueContent = ( setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)); }} > - Validate + {t('common:validationActions.validate')} </EuiButton> </EuiPopoverFooter> ); @@ -737,7 +760,9 @@ const PopoverValueContent = ( <> <EuiFlexItem> <EuiFieldText - placeholder={'Type values'} + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} value={searchFields[index].values} onChange={(e) => setSearchFields( @@ -757,7 +782,7 @@ const PopoverValueContent = ( ); }} > - Validate + {t('common:validationActions.validate')} </EuiButton> </EuiPopoverFooter> </> @@ -767,7 +792,9 @@ const PopoverValueContent = ( <> <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> <EuiComboBox - placeholder={'Select values'} + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.selectValues' + )} options={getListFieldValues()} selectedOptions={searchFields[index].values} onChange={(selectedOptions) => { @@ -790,7 +817,7 @@ const PopoverValueContent = ( ); }} > - Validate + {t('common:validationActions.validate')} </EuiButton> </EuiPopoverFooter> </> @@ -804,7 +831,9 @@ const PopoverValueContent = ( <> <EuiFlexItem> <EuiFieldText - placeholder={'1st value'} + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.firstValue' + )} value={searchFields[index].values[i].value1} onChange={(e) => { setSearchFields( @@ -823,7 +852,9 @@ const PopoverValueContent = ( </EuiFlexItem> <EuiFlexItem> <EuiFieldText - placeholder={'2nd value'} + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.secondValue' + )} value={searchFields[index].values[i].value2} onChange={(e) => setSearchFields( @@ -849,7 +880,9 @@ const PopoverValueContent = ( <> <EuiFlexItem> <EuiFieldText - placeholder={'Type value'} + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} value={searchFields[index].values[i].value1} onChange={(e) => { setSearchFields( @@ -901,7 +934,9 @@ const PopoverValueContent = ( <> <form className={datePickerStyles.container} noValidate> <TextField - label="between" + label={t( + 'search:advancedSearch.fields.fieldContentPopover.betweenDate' + )} type="date" defaultValue={ !!searchFields[index].values[i].startDate @@ -929,7 +964,9 @@ const PopoverValueContent = ( </form> <form className={datePickerStyles.container} noValidate> <TextField - label="and" + label={t( + 'search:advancedSearch.fields.fieldContentPopover.andDate' + )} type="date" defaultValue={ !!searchFields[index].values[i].endDate @@ -1019,17 +1056,15 @@ const PopoverValueContent = ( } }; -const PopoverValueButton = ( +const PopoverValueButton = ({ index, standardFields, - setStandardFields, searchFields, setSearchFields, isPopoverValueOpen, setIsPopoverValueOpen, valueError, setValueError, - search, setSearch, setSearchCount, fieldCount, @@ -1040,8 +1075,10 @@ const PopoverValueButton = ( selectedSources, setSelectedSources, availableSources, - setAvailableSources -) => { + setAvailableSources, +}) => { + const { t } = useTranslation('search'); + return ( <EuiPopover panelPaddingSize="s" @@ -1055,7 +1092,10 @@ const PopoverValueButton = ( ) } iconType="documentEdit" - title="Give field values" + title={t('search:advancedSearch.fields.fieldContentPopover.addFieldValues')} + aria-label={t( + 'search:advancedSearch.fields.fieldContentPopover.addFieldValues' + )} /> } isOpen={isPopoverValueOpen[index]} @@ -1064,35 +1104,33 @@ const PopoverValueButton = ( } > <div style={{ width: 240 }}> - {PopoverValueContent( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - isPopoverValueOpen, - setIsPopoverValueOpen, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources - )} + <PopoverValueContent + index={index} + standardFields={standardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + valueError={valueError} + setValueError={setValueError} + setSearch={setSearch} + setSearchCount={setSearchCount} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + isPopoverValueOpen={isPopoverValueOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + selectedOperatorId={selectedOperatorId} + datePickerStyles={datePickerStyles} + createPolicyToast={createPolicyToast} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + /> </div> </EuiPopover> ); }; -const FieldsPanel = ( +const FieldsPanel = ({ standardFields, setStandardFields, searchFields, @@ -1120,8 +1158,10 @@ const FieldsPanel = ( setSelectedSources, sources, datePickerStyles, - createPolicyToast -) => { + createPolicyToast, +}) => { + const { t } = useTranslation('search'); + const countFieldValues = (field, index) => { const fieldStr = `{${fieldValuesToString(field)}}`; const queriesWithIndices = createAdvancedQueriesBySource( @@ -1144,7 +1184,7 @@ const FieldsPanel = ( }; const handleClearValues = (index) => { - let updatedSearchFields = []; + let updatedSearchFields; switch (searchFields[index].type) { case 'Text': updatedSearchFields = updateArrayElement( @@ -1192,13 +1232,13 @@ const FieldsPanel = ( }; if (standardFields === []) { - return <h2>Loading user fields...</h2>; + return <h2>{t('search:advancedSearch.fields.loadingFields')}</h2>; } return ( <> <EuiTitle size="xs"> - <h2>Field search</h2> + <h2>{t('search:advancedSearch.fields.title')}</h2> </EuiTitle> <EuiPanel paddingSize="m"> <EuiFlexGroup direction="column"> @@ -1212,7 +1252,8 @@ const FieldsPanel = ( color="danger" onClick={() => handleRemoveField(index)} iconType="indexClose" - title="Remove field" + title={t('search:advancedSearch.fields.removeFieldButton')} + aria-label={t('search:advancedSearch.fields.removeFieldButton')} /> </EuiFlexItem> <EuiFlexItem> @@ -1243,70 +1284,61 @@ const FieldsPanel = ( )} </EuiFlexItem> <EuiFlexItem grow={false}> - {isNaN(fieldCount[index]) ? ( - <></> - ) : ( - <> - <EuiTextColor color="secondary"> - {fieldCount[index]}{' '} - {fieldCount[index] === 1 ? 'result' : 'results'} - </EuiTextColor> - </> + {!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="Count results" - /> - </> - ) : ( - <></> + {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="Clear values" - /> - </> - ) : ( - <></> + {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> <EuiFlexItem grow={false}> - {PopoverValueButton( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources - )} + <PopoverValueButton + index={index} + standardFields={standardFields} + setStandardFields={setStandardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + isPopoverValueOpen={isPopoverValueOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + valueError={valueError} + setValueError={setValueError} + search={search} + setSearch={setSearch} + setSearchCount={setSearchCount} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + selectedOperatorId={selectedOperatorId} + datePickerStyles={datePickerStyles} + createPolicyToast={createPolicyToast} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + /> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> @@ -1314,26 +1346,28 @@ const FieldsPanel = ( ))} </EuiFlexGroup> <EuiSpacer size="l" /> - {PopoverSelect( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - fieldCount, - setFieldCount, - selectedSources, - setSelectedSources - )} + <PopoverSelect + standardFields={standardFields} + setStandardFields={setStandardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + selectedField={selectedField} + setSelectedField={setSelectedField} + selectedSection={selectedSection} + setSelectedSection={setSelectedSection} + isPopoverSelectOpen={isPopoverSelectOpen} + setIsPopoverSelectOpen={setIsPopoverSelectOpen} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + /> </EuiPanel> <EuiSpacer size="s" /> <EuiRadioGroup - options={Operators} + options={Operators.map((operator) => { + return { ...operator, label: t(operator.label) }; + })} idSelected={selectedOperatorId} onChange={(id) => { setSelectedOperatorId(id); @@ -1341,73 +1375,72 @@ const FieldsPanel = ( }} name="operators group" legend={{ - children: <span>Search option</span>, + children: <span>{t('search:advancedSearch.searchOptions.title')}</span>, }} /> </> ); }; -const SourceSelect = ( +const SourceSelect = ({ availableSources, selectedSources, setSelectedSources, sourceSelectError, - setSourceSelectError -) => { - if (Object.keys(availableSources).length !== 0) { - availableSources.forEach((source) => { - if (source.name) { - source = changeNameToLabel(source); - } - }); - - const onSourceChange = (selectedOptions) => { - setSourceSelectError(undefined); - setSelectedSources(selectedOptions); - }; + setSourceSelectError, +}) => { + const { t } = useTranslation('search'); - const onSourceSearchChange = (value, hasMatchingOptions) => { - setSourceSelectError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - return ( - <> - <EuiTitle size="xs"> - <h2>Partner sources</h2> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiFlexItem> - <EuiFormRow - error={sourceSelectError} - isInvalid={sourceSelectError !== undefined} - > - <EuiComboBox - placeholder="By default, all sources are selected" - options={availableSources} - selectedOptions={selectedSources} - onChange={onSourceChange} - onSearchChange={onSourceSearchChange} - /> - </EuiFormRow> - </EuiFlexItem> - </> - ); - } else { + if (Object.keys(availableSources).length === 0) { return ( <p> - <EuiIcon type="alert" color="danger" /> No source available ! + <EuiIcon type="alert" color="danger" /> </p> ); } + availableSources.forEach((source) => { + if (source.name) { + source = changeNameToLabel(source); + } + }); + + const onSourceChange = (selectedOptions) => { + setSourceSelectError(undefined); + setSelectedSources(selectedOptions); + }; + + const onSourceSearchChange = (value, hasMatchingOptions) => { + if (value.length === 0 || hasMatchingOptions) { + setSourceSelectError(undefined); + } else { + setSourceSelectError( + t('search:advancedSearch.errorInvalidOption', { value: value }) + ); + } + }; + + return ( + <> + <EuiTitle size="xs"> + <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> + </> + ); }; const AdvancedSearch = ({ - isLoading, - setIsLoading, search, setSearch, searchResults, @@ -1439,11 +1472,7 @@ const AdvancedSearch = ({ setUserHistory, selectedSavedSearch, setSelectedSavedSearch, - historySelectError, - setHistorySelectError, selectedOperatorId, - notificationToasts, - setNotificationToasts, setIsAdvancedSearch, isAdvancedSearch, selectedField, @@ -1462,20 +1491,20 @@ const AdvancedSearch = ({ datePickerStyles, setSourceSelectError, }) => { + const { t } = useTranslation('search'); + const [notificationToasts, setNotificationToasts] = useState([]); + const createPolicyToast = () => { const toast = { - title: 'Policy field selected', + title: t('search:advancedSearch.policyToast.title'), color: 'warning', iconType: 'alert', toastLifeTimeMs: 15000, text: ( <> - <p>You selected a private field.</p> - <p> - Access to this field was granted for specific sources, which means that your - search will be restricted to those. - </p> - <p>Please check the sources list before searching.</p> + <p>{t('search:advancedSearch.policyToast.content.0')}</p> + <p>{t('search:advancedSearch.policyToast.content.1')}</p> + <p>{t('search:advancedSearch.policyToast.content.2')}</p> </> ), }; @@ -1484,25 +1513,18 @@ const AdvancedSearch = ({ const createEditableQueryToast = () => { const toast = { - title: 'Proceed with caution', + title: t('search:advancedSearch.policyToast.title'), color: 'warning', iconType: 'alert', toastLifeTimeMs: 15000, text: ( <> - <p> - Be aware that manually editing the query can spoil search results. The syntax - must be respected : - </p> - <ul> - Fields and their values should be put between brackets : { } - Make - sure every opened bracket is properly closed - </ul> + <p>{t('search:advancedSearch.editableQueryToast.content.part1')}</p> <ul> - "AND" and "OR" should be capitalized between different fields conditions and - lowercased within a field expression + <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> - <ul>Make sure to check eventual typing mistakes</ul> </> ), }; @@ -1525,7 +1547,7 @@ const AdvancedSearch = ({ setIsAdvancedSearch(!isAdvancedSearch); }} > - Switch to basic search + {t('search:advancedSearch.switchSearchMode')} </EuiButtonEmpty> </EuiFlexItem> </EuiFlexGroup> @@ -1533,8 +1555,6 @@ const AdvancedSearch = ({ <EuiFlexItem> <EuiSpacer size="s" /> <SearchBar - isLoading={isLoading} - setIsLoading={setIsLoading} search={search} setSearch={setSearch} searchResults={searchResults} @@ -1565,8 +1585,6 @@ const AdvancedSearch = ({ setUserHistory={setUserHistory} selectedSavedSearch={selectedSavedSearch} setSelectedSavedSearch={setSelectedSavedSearch} - historySelectError={historySelectError} - setHistorySelectError={setHistorySelectError} selectedOperatorId={selectedOperatorId} createEditableQueryToast={createEditableQueryToast} /> @@ -1575,44 +1593,44 @@ const AdvancedSearch = ({ <EuiFlexGroup> <EuiFlexItem> <EuiSpacer size="s" /> - {FieldsPanel( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - selectedOperatorId, - setSelectedOperatorId, - fieldCount, - setFieldCount, - availableSources, - setAvailableSources, - selectedSources, - setSelectedSources, - sources, - datePickerStyles, - createPolicyToast - )} + <FieldsPanel + standardFields={standardFields} + setStandardFields={setStandardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + selectedField={selectedField} + setSelectedField={setSelectedField} + selectedSection={selectedSection} + setSelectedSection={setSelectedSection} + isPopoverSelectOpen={isPopoverSelectOpen} + setIsPopoverSelectOpen={setIsPopoverSelectOpen} + isPopoverValueOpen={isPopoverValueOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + valueError={valueError} + setValueError={setValueError} + search={search} + setSearch={setSearch} + setSearchCount={setSearchCount} + selectedOperatorId={selectedOperatorId} + setSelectedOperatorId={setSelectedOperatorId} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + sources={sources} + datePickerStyles={datePickerStyles} + createPolicyToast={createPolicyToast} + /> <EuiSpacer size="s" /> - {SourceSelect( - availableSources, - selectedSources, - setSelectedSources, - sourceSelectError, - setSourceSelectError - )} + <SourceSelect + availableSources={availableSources} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + sourceSelectError={sourceSelectError} + setSourceSelectError={setSourceSelectError} + /> </EuiFlexItem> </EuiFlexGroup> <EuiGlobalToastList diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js index fc9710d..6f8892d 100644 --- a/src/pages/search/BasicSearch/BasicSearch.js +++ b/src/pages/search/BasicSearch/BasicSearch.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -18,14 +18,13 @@ const BasicSearch = ({ selectedSources, basicSearch, setBasicSearch, - isLoading, setIsAdvancedSearch, isAdvancedSearch, - setIsLoading, setSearchResults, setSelectedTabNumber, }) => { const { t } = useTranslation('search'); + const [isLoading, setIsLoading] = useState(false); const onFormSubmit = () => { setIsLoading(true); @@ -38,7 +37,9 @@ const BasicSearch = ({ searchQuery(queriesWithIndices).then((result) => { setSearchResults(result); setSelectedTabNumber(1); - setIsLoading(false); + if (isLoading) { + setIsLoading(false); + } }); }; @@ -59,7 +60,7 @@ const BasicSearch = ({ <EuiFlexGroup> <EuiFlexItem> <EuiSpacer size="s" /> - <form onSubmit={onFormSubmit}> + <form onSubmit={() => onFormSubmit()}> <EuiFlexGroup> <EuiFlexItem> <EuiFieldSearch @@ -71,7 +72,7 @@ const BasicSearch = ({ </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton type="submit" fill isDisabled={isAdvancedSearch}> - {t('basicSearch.sendSearchButton')} + {t('sendSearchButton')} </EuiButton> </EuiFlexItem> </EuiFlexGroup> diff --git a/src/pages/search/Data.js b/src/pages/search/Data.js index c657764..d0a399a 100644 --- a/src/pages/search/Data.js +++ b/src/pages/search/Data.js @@ -2,12 +2,12 @@ export const Operators = [ { id: '0', value: 'And', - label: 'Match all criterias', + label: 'search:advancedSearch.searchOptions.matchAll', }, { id: '1', value: 'Or', - label: 'Match at least one criteria', + label: 'search:advancedSearch.searchOptions.matchAtLeastOne', }, ]; diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 256ce8d..32b18a2 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -19,30 +19,15 @@ import { fetchUserPolicyFields, fetchSources, } from '../../actions/source'; -import { fetchUserHistory } from '../../actions/user'; import { useTranslation } from 'react-i18next'; import AdvancedSearch from './AdvancedSearch/AdvancedSearch'; import BasicSearch from './BasicSearch/BasicSearch'; import styles from './styles'; -export const fetchHistory = (setUserHistory) => { - fetchUserHistory(sessionStorage.getItem('user_id')).then((result) => { - if (result[0] && result[0].ui_structure) { - result.forEach((item) => { - item.ui_structure = JSON.parse(item.ui_structure); - item.label = `${item.name} - ${new Date(item.createdat).toLocaleString()}`; - }); - } - setUserHistory(result); - }); -}; - const Search = () => { const { t } = useTranslation('search'); const datePickerStyles = styles(); - const [isLoading, setIsLoading] = useState(false); const [selectedTabNumber, setSelectedTabNumber] = useState(0); - const [userHistory, setUserHistory] = useState({}); const [isAdvancedSearch, setIsAdvancedSearch] = useState(false); const [readOnlyQuery, setReadOnlyQuery] = useState(true); const [selectedField, setSelectedField] = useState([]); @@ -67,8 +52,6 @@ const Search = () => { const [isReadOnlyModalOpen, setIsReadOnlyModalOpen] = useState(false); const [isSaveSearchModalOpen, setIsSaveSearchModalOpen] = useState(false); const [selectedSavedSearch, setSelectedSavedSearch] = useState(); - const [historySelectError, setHistorySelectError] = useState(undefined); - const [notificationToasts, setNotificationToasts] = useState([]); useEffect(() => { fetchPublicFields().then((resultStdFields) => { @@ -104,7 +87,6 @@ const Search = () => { setSources(result); setAvailableSources(result); }); - fetchHistory(setUserHistory); }, []); const tabsContent = [ @@ -115,8 +97,6 @@ const Search = () => { <> {isAdvancedSearch ? ( <AdvancedSearch - isLoading={isLoading} - setIsLoading={setIsLoading} search={search} setSearch={setSearch} searchResults={searchResults} @@ -144,15 +124,9 @@ const Search = () => { setIsReadOnlyModalOpen={setIsReadOnlyModalOpen} isSaveSearchModalOpen={isSaveSearchModalOpen} setIsSaveSearchModalOpen={setIsSaveSearchModalOpen} - userHistory={userHistory} - setUserHistory={setUserHistory} selectedSavedSearch={selectedSavedSearch} setSelectedSavedSearch={setSelectedSavedSearch} - historySelectError={historySelectError} - setHistorySelectError={setHistorySelectError} selectedOperatorId={selectedOperatorId} - notificationToasts={notificationToasts} - setNotificationToasts={setNotificationToasts} setIsAdvancedSearch={setIsAdvancedSearch} isAdvancedSearch={isAdvancedSearch} selectedField={selectedField} @@ -180,8 +154,6 @@ const Search = () => { selectedSources={selectedSources} basicSearch={basicSearch} setBasicSearch={setBasicSearch} - isLoading={isLoading} - setIsLoading={setIsLoading} setSearchResults={setSearchResults} setSelectedTabNumber={setSelectedTabNumber} /> -- GitLab From 550ee628fa6dd326c30d3d022e5a36a0b86fc2e2 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Wed, 15 May 2024 17:45:42 +0200 Subject: [PATCH 14/17] [AdvancedSearch.js]: corrected a React memory leak error caused by isLoading bool --- src/pages/search/AdvancedSearch/AdvancedSearch.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js index cc2030a..cd4b8a0 100644 --- a/src/pages/search/AdvancedSearch/AdvancedSearch.js +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -291,7 +291,9 @@ const SearchBar = ({ searchQuery(queriesWithIndices).then((result) => { setSearchResults(result); setSelectedTabNumber(1); - setIsLoading(false); + if (isLoading) { + setIsLoading(false); + } }); } }; -- GitLab From 8119aadc1cee82589d18b80517bdfda39547937b Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Wed, 15 May 2024 17:50:49 +0200 Subject: [PATCH 15/17] [Results]: corrected #6 bug, causing query text not to update when switching to basic search from advancedSearch --- src/pages/results/Results.js | 10 +++++----- src/pages/search/Search.js | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/results/Results.js b/src/pages/results/Results.js index e1dd381..1d75887 100644 --- a/src/pages/results/Results.js +++ b/src/pages/results/Results.js @@ -38,17 +38,17 @@ const changeFlyoutState = (array, index, value, defaultValue) => { return newArray; }; -const Results = ({ searchResults, search, basicSearch }) => { +const Results = ({ searchResults, searchQuery }) => { const { t } = useTranslation('results'); const [resultsCol, setResultsCol] = useState([]); const [results, setResults] = useState([]); const [isFlyoutOpen, setIsFlyoutOpen] = useState([false]); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQueryText, setSearchQueryText] = useState(''); useEffect(() => { processData(searchResults); - search.length ? setSearchQuery(search) : setSearchQuery(basicSearch); - }, [searchResults, search, basicSearch]); + setSearchQueryText(searchQuery); + }, [searchResults]); const updateTableCell = (tableContent, value, colIndex, rowIndex) => { const updatedRow = updateArrayElement(tableContent[rowIndex], colIndex, value); @@ -330,7 +330,7 @@ const Results = ({ searchResults, search, basicSearch }) => { <EuiSpacer size="s" /> <EuiFlexItem grow={false}> <EuiTitle size="xs"> - <h2>Your query : {searchQuery}</h2> + <h2>Your query : {searchQueryText}</h2> </EuiTitle> </EuiFlexItem> <EuiSpacer size="s" /> diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 32b18a2..61bd93e 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -169,8 +169,7 @@ const Search = () => { <EuiFlexItem> <Results searchResults={searchResults} - search={search} - basicSearch={basicSearch} + searchQuery={isAdvancedSearch ? search : basicSearch} /> </EuiFlexItem> </EuiFlexGroup> -- GitLab From 1077ca8592712d78e1806a155f1d33b8f3cb66cc Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Thu, 16 May 2024 11:49:09 +0200 Subject: [PATCH 16/17] [Results.js]: added full translations and cleaned component; improved results table columns names display --- public/locales/en/results.json | 5 ++ public/locales/fr/results.json | 5 ++ src/pages/results/Results.js | 157 ++++----------------------------- 3 files changed, 25 insertions(+), 142 deletions(-) diff --git a/public/locales/en/results.json b/public/locales/en/results.json index 742539e..61cf048 100644 --- a/public/locales/en/results.json +++ b/public/locales/en/results.json @@ -1,5 +1,10 @@ { + "yourQuery": "Your query: {{query}}", + "clickOnRowTip": "Click on a resource row to display full metadata.", "downloadResultsButton": { "JSON": "Download as JSON" + }, + "table": { + "title": "Search results" } } diff --git a/public/locales/fr/results.json b/public/locales/fr/results.json index 498fc27..e611299 100644 --- a/public/locales/fr/results.json +++ b/public/locales/fr/results.json @@ -1,5 +1,10 @@ { + "yourQuery": "Votre requête : {{query}}", + "clickOnRowTip": "Clickez sur une ressource (ligne du tableau) pour afficher ses métadonnées.", "downloadResultsButton": { "JSON": "Télécharger en JSON" + }, + "table": { + "title": "Résultats de la recherche" } } diff --git a/src/pages/results/Results.js b/src/pages/results/Results.js index 1d75887..b647f77 100644 --- a/src/pages/results/Results.js +++ b/src/pages/results/Results.js @@ -63,13 +63,20 @@ const Results = ({ searchResults, searchQuery }) => { return updatedResults; }; + const buildColumnName = (name) => { + // Replace underscore with spaces + name = name.split('_').join(' '); + // Uppercase first character + name = name.charAt(0).toUpperCase() + name.slice(1); + return name; + }; + const processData = (metadata) => { if (metadata) { const columns = []; const rows = []; - // const metadataRecords = metadata.hits.hits columns.push({ - name: 'currently open', + name: 'Currently open', options: { display: true, viewColumns: true, @@ -88,7 +95,7 @@ const Results = ({ searchResults, searchQuery }) => { if (typeof displayedFields[fieldName] === 'string') { if (recordIndex === 0) { const column = { - name: fieldName, + name: buildColumnName(fieldName), options: { display: true, }, @@ -156,13 +163,11 @@ const Results = ({ searchResults, searchQuery }) => { filterType: 'dropdown', responsive: 'standard', selectableRows: 'none', - selectableRowsOnClick: false, + selectableRowsOnClick: true, onRowSelectionChange: (rowsSelected, allRows) => {}, onRowClick: (rowData, rowState) => {}, onCellClick: (val, colMeta) => { - // if (searchResults.hits.hits && colMeta.colIndex !== 0) { if (searchResults && colMeta.colIndex !== 0) { - // const updatedTable = updateTableCell(closeAllFlyouts(results), recordFlyout(searchResults.hits.hits[colMeta.rowIndex]._source, colMeta.rowIndex, !isFlyoutOpen[colMeta.rowIndex]), 0, colMeta.rowIndex) const updatedTable = updateTableCell( closeAllFlyouts(results), recordFlyout( @@ -185,134 +190,6 @@ const Results = ({ searchResults, searchQuery }) => { }, }; - /* const displayRecord = (record) => { - let recordDisplay = [] - if (!!record) { - const fields = Object.keys(record) - fields.forEach(field => { - if (typeof record[field] != 'string') { - // const rndId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - if (isNaN(field)) { - // const buttonContent = `"${field}"` - let isStrArray = false - if (Array.isArray(record[field])) { - isStrArray = true - record[field].forEach(item => { - if (typeof item != 'string') - isStrArray = false - }) - } - if (isStrArray) { - recordDisplay.push( - <> - <h3> -  {field} - </h3> - {displayRecord(record[field])} - </> - ) - } else { - recordDisplay.push( - <> - <EuiSpacer size="s" /> - <EuiPanel paddingSize="s"> - <EuiAccordion id={Math.random().toString()} buttonContent={field}> - <EuiText size="s"> - {displayRecord(record[field])} - </EuiText> - </EuiAccordion> - </EuiPanel> - </> - ) - } - } else { - recordDisplay.push( - <> - {displayRecord(record[field])} - </> - ) - if (fields[fields.indexOf(field) + 1]) - recordDisplay.push( - <> - <EuiSpacer size="m" /> - <hr /> - </> - ) - } - } else { - if (isNaN(field)) { - recordDisplay.push( - <> - <h3> -  {field} - </h3> - <EuiTextColor color="secondary">  {record[field]}</EuiTextColor> - </> - ) - } else { - recordDisplay.push( - <> - <EuiSpacer size="s" /> - <EuiTextColor color="secondary">  {record[field]}</EuiTextColor> - </> - ) - } - } - }) - return recordDisplay - } - } - - const recordFlyout = (record, recordIndex, isFlyoutOpen, setIsFlyoutOpen) => { - let flyout - if (isFlyoutOpen[recordIndex]) { - // const flyOutContent = ReactHtmlParser(displayRecord(record, 1)) - const flyOutStr = displayRecord(record) - // const flyOutContent = parse(flyOutStr, { htmlparser2: { lowerCaseTags: false } }) - const flyout = ( - <> - <EuiFlyout - onClose={() => { - // setIsFlyoutOpen(updateArrayElement(isFlyoutOpen, recordIndex, false)) - // updateResultsCell(false, 0, recordIndex) - const updatedArray = changeFlyoutState(isFlyoutOpen, recordIndex, !isFlyoutOpen[recordIndex], false) - setIsFlyoutOpen(updatedArray) - }} - aria-labelledby={recordIndex}> - <EuiFlyoutBody> - <EuiText size="s"> - <Fragment> - {flyOutStr} - </Fragment> - </EuiText> - </EuiFlyoutBody> - </EuiFlyout> - <EuiIcon type='eye' color='danger' /> - </> - ); - return (flyout) - } - } */ - - /* const viewButton = (record, recordIndex, isFlyoutOpenIndex, isFlyoutOpen, setIsFlyoutOpen) => { - return ( - <> - <EuiButtonIcon - size="m" - color="success" - onClick={() => { - const flyOutArray = updateArrayElement(isFlyoutOpen, recordIndex, !isFlyoutOpen[recordIndex]) - setIsFlyoutOpen(flyOutArray) - updateResultsCell(!isFlyoutOpen[recordIndex], isFlyoutOpenIndex, recordIndex) - }} - iconType="eye" - title="View record" - /> - {recordFlyout(record, recordIndex, isFlyoutOpen, setIsFlyoutOpen)} - </> - ) - } */ - const downloadResults = () => { if (searchResults) { download( @@ -330,28 +207,24 @@ const Results = ({ searchResults, searchQuery }) => { <EuiSpacer size="s" /> <EuiFlexItem grow={false}> <EuiTitle size="xs"> - <h2>Your query : {searchQueryText}</h2> + <h2>{t('results:yourQuery', { query: searchQueryText })}</h2> </EuiTitle> </EuiFlexItem> <EuiSpacer size="s" /> </EuiFlexGroup> <EuiFlexGroup> <EuiFlexItem> - <EuiCallOut - size="s" - title="Click on a line of the table to inspect resource metadata (except for the first column)." - iconType="search" - /> + <EuiCallOut size="s" title={t('results:clickOnRowTip')} iconType="search" /> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton fill onClick={() => downloadResults()}> - {t('downloadResultsButton.JSON')} + {t('results:downloadResultsButton.JSON')} </EuiButton> </EuiFlexItem> </EuiFlexGroup> <MuiThemeProvider theme={getMuiTheme()}> <MUIDataTable - title={'Search results'} + title={t('results:table.title')} data={results} columns={resultsCol} options={resultsGridOptions} -- GitLab From 7d1c0632338a0b537bcec55abcbe1753ac2196aa Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 17 May 2024 03:40:39 +0200 Subject: [PATCH 17/17] [SearchMaps]: added full translations ; added a tool column in legend table --- public/locales/en/maps.json | 14 +++++- public/locales/fr/maps.json | 16 +++++-- src/pages/maps/SearchMap.js | 85 +++++++++++++++++++------------------ src/pages/maps/styles.js | 2 - 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/public/locales/en/maps.json b/public/locales/en/maps.json index 08ae921..ca5269d 100644 --- a/public/locales/en/maps.json +++ b/public/locales/en/maps.json @@ -2,12 +2,22 @@ "layersChoiceTitle": "Click on layers to toggle display.", "layersTableHeaders": { "cartography": "Cartography", - "data": "Data" + "filters": "Filters", + "tools": "Tools" }, "layersTable": { "openStreetMap": "Open Street Map", "bingAerial": "Bing Aerial", "IGN": "IGN map", - "queryResults": "Query results" + "SylvoEcoRegions": "SylvoEcoRegions", + "queryResults": "Query results", + "regions": "Regions", + "departments": "Departments", + "selectFilterOption": "Select a single option", + "selectedPointsList": "Selected resources list", + "pointSelectionMode": { + "select": "Select", + "unselect": "Unselect" + } } } diff --git a/public/locales/fr/maps.json b/public/locales/fr/maps.json index a03e165..c113605 100644 --- a/public/locales/fr/maps.json +++ b/public/locales/fr/maps.json @@ -2,12 +2,22 @@ "layersChoiceTitle": "Cliquez sur les couches pour modifier l'affichage.", "layersTableHeaders": { "cartography": "Cartographie", - "data": "Données" + "filters": "Filtres", + "tools": "Outils" }, "layersTable": { "openStreetMap": "Open Street Map", - "bingAerial": "Bing carte", + "bingAerial": "Bing vue aérienne", "IGN": "Plan IGN", - "queryResults": "Résultats de la requête" + "SylvoEcoRegions": "SylvoEcoRégions", + "queryResults": "Résultats de la requête", + "regions": "Régions", + "departments": "Départements", + "selectFilterOption": "Sélectionnez une option", + "selectedPointsList": "Liste des resources sélectionnées", + "pointSelectionMode": { + "select": "Sélection", + "unselect": "Désélection" + } } } diff --git a/src/pages/maps/SearchMap.js b/src/pages/maps/SearchMap.js index 378e263..5ba404c 100644 --- a/src/pages/maps/SearchMap.js +++ b/src/pages/maps/SearchMap.js @@ -54,19 +54,18 @@ const initMatrixIds = () => { return matrixIds; }; -const layersOptions = [ - { label: 'Résultats de la requête', value: 'ResRequete' }, - { label: 'Régions', value: 'regions' }, - { label: 'Départements', value: 'departements' }, -]; - const SearchMap = ({ searchResults }) => { const { t } = useTranslation('maps'); const [center, setCenter] = useState(proj.fromLonLat([2.5, 46.5])); const [zoom, setZoom] = useState(6); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [deselectChecked, setDeselectChecked] = useState(false); - const [selectedOptions, setSelected] = useState([layersOptions[0]]); + const filterOptions = [ + { label: t('maps:layersTable.queryResults'), value: 'ResRequete' }, + { label: t('maps:layersTable.regions'), value: 'regions' }, + { label: t('maps:layersTable.departments'), value: 'departements' }, + ]; + const [selectedOptions, setSelected] = useState([filterOptions[0]]); const resultPointsSource4BG = new VectorSource({}); const resultPointsSource = new VectorSource({}); let selectedPointsSource = new VectorSource({}); @@ -87,7 +86,7 @@ const SearchMap = ({ searchResults }) => { style.setStroke(stroke); return style; }; - const maps4Filter = { + const mapFilters = { ResRequete: new VectorLayer({ name: 'ResRequete', source: resultPointsSource, @@ -181,7 +180,7 @@ const SearchMap = ({ searchResults }) => { name: 'queryResults', source: resultPointsSource4BG, }), - maps4Filter['selectedPointsLayer'], + mapFilters['selectedPointsLayer'], ]); const [mapLayersVisibility, setMapLayersVisibility] = useState( new Array(mapLayers.length).fill(false) @@ -229,6 +228,7 @@ const SearchMap = ({ searchResults }) => { setZoom(map.getView().getZoom()); }); map.addInteraction(dragBox); + map.addInteraction(select); // clear selection when drawing a new box and when clicking on the map dragBox.on('boxstart', function () { selectedFeatures.clear(); @@ -252,7 +252,7 @@ const SearchMap = ({ searchResults }) => { if (selectedOptions[0].value === 'queryResults') { polygonSource = pointsSource; } else { - polygonSource = maps4Filter[selectedOptions[0].value].get('source'); + polygonSource = mapFilters[selectedOptions[0].value].get('source'); } const boxFeatures = polygonSource .getFeaturesInExtent(extent) @@ -304,7 +304,6 @@ const SearchMap = ({ searchResults }) => { selectedFeatures.extend(boxFeatures); } }); - map.addInteraction(select); map.getView().animate({ zoom: zoom }, { center: center }, { duration: 2000 }); processData(); }, [searchResults, map, deselectChecked]); @@ -321,13 +320,13 @@ const SearchMap = ({ searchResults }) => { setIsPopoverOpen(false); }; - const onLayersSelectChange = (newSelectedOptions) => { + const onFilterSelectChange = (newSelectedOptions) => { setSelected(newSelectedOptions); const allMyLayers = map.getLayers(); - // remove previously selected layer - for (let i = 0; i < layersOptions.length; i++) { + // remove previously selected filter + for (let i = 0; i < filterOptions.length; i++) { for (let j = 0; j < allMyLayers.getLength(); j++) { - if (layersOptions[i].value === map.getLayers().item(j).get('name')) { + if (filterOptions[i].value === map.getLayers().item(j).get('name')) { map.removeLayer(map.getLayers().item(j)); break; } @@ -340,7 +339,7 @@ const SearchMap = ({ searchResults }) => { if (selectedMap === 'ResRequete') { toggleLayer('queryResults'); } else { - map.addLayer(maps4Filter[selectedMap]); + map.addLayer(mapFilters[selectedMap]); } }; @@ -444,7 +443,8 @@ const SearchMap = ({ searchResults }) => { <thead> <tr> <th>{t('layersTableHeaders.cartography')}</th> - <th>{t('layersTableHeaders.data')}</th> + <th>{t('layersTableHeaders.filters')}</th> + <th>{t('layersTableHeaders.tools')}</th> </tr> </thead> <tbody> @@ -485,31 +485,42 @@ const SearchMap = ({ searchResults }) => { </li> </ul> </td> - <td> + <td style={styles.layersTableCells}> <ul> - <input - type="checkbox" - checked={deselectChecked} - onChange={handleDeselectCheckboxChange} + <EuiCheckbox + id={htmlIdGenerator()()} + label={t('layersTable.queryResults')} + checked={mapLayersVisibility[getLayerIndex('queryResults')]} + onChange={() => toggleLayer('queryResults')} /> - <i>Désélectionner</i> + <br /> <EuiComboBox - aria-label="Accessible screen reader label" - placeholder="Select a single option" + aria-label={t('maps:layersTable.selectFilterOption')} + placeholder={t('maps:layersTable.selectFilterOption')} singleSelection={{ asPlainText: true }} - options={layersOptions} + options={filterOptions} selectedOptions={selectedOptions} - onChange={onLayersSelectChange} + onChange={onFilterSelectChange} + /> + </ul> + </td> + <td style={styles.layersTableCells}> + <ul> + <input + type="checkbox" + checked={deselectChecked} + onChange={handleDeselectCheckboxChange} /> + <i> + {deselectChecked + ? t('maps:layersTable.pointSelectionMode.select') + : t('maps:layersTable.pointSelectionMode.unselect')} + </i> <br /> <EuiPopover button={ - <EuiButtonEmpty - iconType="documentation" - iconSide="right" - onClick={onButtonClick} - > - Liste des résultats sélectionnés + <EuiButtonEmpty onClick={onButtonClick}> + {t('maps:layersTable.selectedPointsList')} </EuiButtonEmpty> } isOpen={isPopoverOpen} @@ -529,14 +540,6 @@ const SearchMap = ({ searchResults }) => { </EuiPopover> </ul> </td> - <td style={styles.layersTableCells}> - <EuiCheckbox - id={htmlIdGenerator()()} - label={t('layersTable.queryResults')} - checked={mapLayersVisibility[getLayerIndex('queryResults')]} - onChange={(e) => toggleLayer('queryResults')} - /> - </td> </tr> </tbody> </table> diff --git a/src/pages/maps/styles.js b/src/pages/maps/styles.js index ac6cb6b..72257f8 100644 --- a/src/pages/maps/styles.js +++ b/src/pages/maps/styles.js @@ -35,11 +35,9 @@ const styles = { }, layersTable: { margin: '20px', - border: '1px solid black', }, layersTableCells: { padding: '10px', - verticalAlign: 'top', }, }; -- GitLab