From 6bdb3b0492d6a72d150150bf6d60bf2ef3d97248 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Mon, 3 Feb 2025 10:03:10 +0100
Subject: [PATCH 1/3] [BasicSearch] added error handling on search request ;
 added a HowTo component [Layout] improved styling and code logic

---
 public/locales/en/search.json                 |  69 +++++++++-
 public/locales/fr/search.json                 |  67 +++++++++-
 src/Utils.js                                  |   1 -
 src/components/Layout/Layout.js               |  48 +++----
 src/components/Layout/styles.js               |   6 +-
 src/pages/search/BasicSearch/BasicSearch.js   |  20 ++-
 .../search/BasicSearch/HowToBasicSearch.js    | 120 ++++++++++++++++++
 7 files changed, 298 insertions(+), 33 deletions(-)
 create mode 100644 src/pages/search/BasicSearch/HowToBasicSearch.js

diff --git a/public/locales/en/search.json b/public/locales/en/search.json
index 67d35bc..b25f4c6 100644
--- a/public/locales/en/search.json
+++ b/public/locales/en/search.json
@@ -5,10 +5,75 @@
     "map": "Map"
   },
   "sendSearchButton": "Search",
+  "queryError": "Your query is not formed correctly.",
   "basicSearch": {
     "switchSearchMode": "Switch to advanced search",
-    "searchInputPlaceholder": "Search..."
-  },
+    "searchInputPlaceholder": "Search...",
+    "howTo": {
+      "toggleAction": "How does basic search works ?",
+      "examplesTitle": "A few examples",
+      "examples": [
+        {
+          "query": "Cedrus",
+          "desc": "searches for Cedrus in any field of the standard."
+        },
+        {
+          "query": "biological_material.species:Cedrus",
+          "desc": "searches for Cedrus <strong>only</strong>in the species field of the standard."
+        },
+        {
+          "query": "Cedr?s Atlanti*",
+          "desc": "searches for Cedrus Atlantica, Cedris Atlanticus, and all other possibilities."
+        },
+        {
+          "query": "\"Cedrus Atlantica\" +(FCBA || INRAE)",
+          "desc": "searches \"Cedrus Atlantica\" and <strong>necessarily</strong> FCBA or INRAE."
+        },
+        {
+          "query": "\"Pinus nigra\" +experimental_site.geo_point.altitude:>1200 +experimental_site.start_date:[1970-01-01 TO 1999-01-01]",
+          "desc": "searches for black pine on an experimental site above 1200m high that started between 1970 and 1999."
+        },
+        {
+          "query": "\"Cedrus Atlantica\" -biological_material.genetic_level:clone",
+          "desc": "searches for Cedrus Atlantica while excluding all clones."
+        }
+      ],
+      "sections": [
+        {
+          "title": "Term based functionnality",
+          "content": [
+            "Each term searches for an <strong>exact</strong> match among all standard fields.",
+            "Colon <strong>: </strong> allows to search in a <strong>specific standard field</strong>.",
+            "Quotes <strong>\" \"</strong> link terms <strong>in one and only</strong> character string."
+          ]
+        }, {
+          "title": "Wildcards",
+          "content": [
+            "<strong>?</strong> matches any unique character.",
+            "<strong>*</strong> matches zero or any multiple character including an empty one."
+          ]
+        }, {
+          "title": "Priorities operators",
+          "content": [
+            "Ampersand <strong>&&</strong> allows a <strong>logical and</strong> between two terms.",
+            "Double pipe <strong>||</strong> allows a <strong>logical or</strong> between two terms.",
+            "Parenthesis <strong>( )</strong> specify priority while using multiple operators.",
+            "<strong>+</strong> forces <strong>presence</strong> of the following terms.",
+            "<strong>-</strong> or <strong>!</strong> forces <strong>absence</strong> of the following terms."
+          ]
+        }, {
+          "title": "Comparison operators",
+          "content": [
+            "<, >, <=, => allows numeric and date comparison. Dates in YYYY-MM-DD format.",
+            "Brackets <strong>[ ]</strong> to search in a inclusive interval.",
+            "Curly brackets <strong>{ }</strong> to search in a exclusive interval.",
+            "Intervals two <strong>endpoints</strong> must be linked with <strong>TO</strong>.",
+            "You can combine two types of brackets in a same interval:  <strong>[ } or { ]</strong>."
+          ]
+        }
+      ]
+    }
+},
   "advancedSearch": {
     "switchSearchMode": "Switch to basic search",
     "textQueryPlaceholder": "Add fields...",
diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json
index 101410a..df6a52b 100644
--- a/public/locales/fr/search.json
+++ b/public/locales/fr/search.json
@@ -5,9 +5,74 @@
     "map": "Carte"
   },
   "sendSearchButton": "Lancer la recherche",
+  "queryError": "Votre requête n'est pas formée correctement.",
   "basicSearch": {
     "switchSearchMode": "Passer en recherche avancée",
-    "searchInputPlaceholder": "Chercher..."
+    "searchInputPlaceholder": "Chercher...",
+    "howTo": {
+      "toggleAction": "Comment fonctionne la recherche basique ?",
+      "examplesTitle": "Quelques exemples",
+      "examples": [
+        {
+          "query": "Cedrus",
+          "desc": "cherche Cedrus parmi l'ensemble des champs du standard."
+        },
+        {
+          "query": "biological_material.species:Cedrus",
+          "desc": "cherche Cedrus <strong>uniquement</strong> dans le champ espèce du standard."
+        },
+        {
+          "query": "Cedr?s Atlanti*",
+          "desc": "cherche Cedrus Atlantica, Cedris Atlanticus, ainsi que toutes les autres possibilités."
+        },
+        {
+          "query": "\"Cedrus Atlantica\" +(FCBA || INRAE)",
+          "desc": "cherche \"Cedrus Atlantica\" et <strong>obligatoirement</strong> un de FCBA ou INRAE."
+        },
+        {
+          "query": "\"Pinus nigra\" +experimental_site.geo_point.altitude:>1200 +experimental_site.start_date:[1970-01-01 TO 1999-01-01]",
+          "desc": "cherche du pin noir sur un site exp. situé à plus de 1200m d'altitude, ayant débuté entre 1970 et 1999."
+        },
+        {
+          "query": "\"Cedrus Atlantica\" -biological_material.genetic_level:clone",
+          "desc": "cherche du Cedrus Atlantica en excluant tous les clônes."
+        }
+      ],
+      "sections": [
+        {
+          "title": "Fonctionnement par mots clés",
+          "content": [
+            "Chaque terme cherche une correspondance <strong>exacte</strong> parmi tous les champs du standard.",
+            "Le <strong>deux-points : </strong>permet de chercher dans un <strong>champ spécifique du standard</strong>.",
+            "Les guillemets <strong>\" \"</strong> lient des termes en <strong>une seule et même</strong> chaîne de caractère."
+          ]
+        }, {
+          "title": "Caractères de remplacement",
+          "content": [
+            "<strong>?</strong> qui correspond à n'importe quel caractère unique.",
+            "<strong>*</strong> qui peut correspondre à zéro ou plusieurs caractères, y compris un caractère vide."
+          ]
+        }, {
+          "title": "Opérateurs de priorités",
+          "content": [
+            "Le et commercial <strong>&&</strong> permet un <strong>et logique</strong> entre deux termes.",
+            "La barre verticale <strong>||</strong> permet un <strong>ou logique</strong> entre deux termes.",
+            "Les parenthèses <strong>( )</strong> précisent la priorité dès que vous utilisez plusieurs opérateurs.",
+            "<strong>+</strong> oblige <strong>la présence</strong> des termes suivants.",
+            "<strong>-</strong> ou <strong>!</strong> obligent <strong>l'absence</strong> des termes suivants."
+          ]
+        }, {
+          "title": "Opérateurs de comparaison",
+          "content": [
+            "<, >, <=, => permettent des comparaisons avec des valeurs numériques ou des dates (au format AAAA-MM-DD).",
+            "Les crochets <strong>[ ]</strong> pour chercher dans un intervalle inclusif.",
+            "Les brackets <strong>{ }</strong> pour chercher dans un intervalle exclusif.",
+            "Les deux <strong>bornes</strong> d'un intervalle doivent être liés par le terme <strong>TO</strong>.",
+            "Vous pouvez combiner un crochet et un bracket dans le même intervalle: <strong>[ } ou { ]</strong>."
+          ]
+        }
+      ]
+    }
   },
   "advancedSearch": {
     "switchSearchMode": "Passer en recherche basique",
diff --git a/src/Utils.js b/src/Utils.js
index 65fc6c4..c51028a 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -433,7 +433,6 @@ export const createAdvancedQueriesBySource = (
   });
 
   const queryContent = buildDslQuery(searchRequest, fields);
-  console.log('dslQuery', queryContent);
 
   sourcesLists.forEach((sourcesArray, index) => {
     let sourceParam = `"_source": [`;
diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js
index 087c5ce..b28f490 100644
--- a/src/components/Layout/Layout.js
+++ b/src/components/Layout/Layout.js
@@ -1,34 +1,38 @@
 import React from 'react';
 import { Outlet } from 'react-router-dom';
 import Header from '../../components/Header';
-import { EuiPage, EuiPageBody, EuiPageSection } from '@elastic/eui';
+import { EuiFlexGroup, EuiPage, EuiPageBody, EuiPageSection } from '@elastic/eui';
 import styles from './styles.js';
 import { Slide, ToastContainer } from 'react-toastify';
 import 'react-toastify/dist/ReactToastify.css';
 
 const Layout = () => {
   return (
-    <EuiPage style={styles.page} restrictWidth={false}>
-      <EuiPageBody>
-        <ToastContainer
-          position="bottom-right"
-          autoClose={5000}
-          hideProgressBar
-          newestOnTop={false}
-          closeOnClick
-          rtl={false}
-          pauseOnFocusLoss
-          draggable
-          pauseOnHover
-          theme="light"
-          transition={Slide}
-        />
-        <Header />
-        <EuiPageSection style={styles.pageContent} grow={true}>
-          <Outlet />
-        </EuiPageSection>
-      </EuiPageBody>
-    </EuiPage>
+    <EuiFlexGroup style={styles.page}>
+      <EuiPage grow={true}>
+        <EuiPageBody>
+          <ToastContainer
+            position="bottom-right"
+            autoClose={5000}
+            hideProgressBar
+            newestOnTop={false}
+            closeOnClick
+            rtl={false}
+            pauseOnFocusLoss
+            draggable
+            pauseOnHover
+            theme="light"
+            transition={Slide}
+          />
+          <Header />
+          <EuiFlexGroup>
+            <EuiPageSection grow={true}>
+              <Outlet />
+            </EuiPageSection>
+          </EuiFlexGroup>
+        </EuiPageBody>
+      </EuiPage>
+    </EuiFlexGroup>
   );
 };
 
diff --git a/src/components/Layout/styles.js b/src/components/Layout/styles.js
index fd435cb..5a2a1fe 100644
--- a/src/components/Layout/styles.js
+++ b/src/components/Layout/styles.js
@@ -1,9 +1,7 @@
 const styles = {
   page: {
-    padding: '0px',
-  },
-  pageContent: {
-    backgroundColor: '#ffffff',
+    minHeight: '100vh',
+    maxHeight: '100vh',
   },
 };
 
diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js
index 77f208a..8f36200 100644
--- a/src/pages/search/BasicSearch/BasicSearch.js
+++ b/src/pages/search/BasicSearch/BasicSearch.js
@@ -9,6 +9,9 @@ import {
 import { useTranslation } from 'react-i18next';
 import SearchModeSwitcher from '../SearchModeSwitcher';
 import { useGatekeeper } from '../../../contexts/GatekeeperContext';
+import HowToBasicSearch from './HowToBasicSearch';
+import { toast } from 'react-toastify';
+import ToastMessage from '../../../components/ToastMessage/ToastMessage';
 
 const BasicSearch = ({
   standardFields,
@@ -20,7 +23,7 @@ const BasicSearch = ({
   setSearchResults,
   setSelectedTabNumber,
 }) => {
-  const { t } = useTranslation('search');
+  const { t } = useTranslation(['search', 'validation']);
   const client = useGatekeeper();
   const [isLoading, setIsLoading] = useState(false);
 
@@ -33,8 +36,18 @@ const BasicSearch = ({
       fieldsId: standardFields,
     });
     setIsLoading(false);
-    setSearchResults(result);
-    setSelectedTabNumber(1);
+    if (!result.error) {
+      setSearchResults(result);
+      setSelectedTabNumber(1);
+    } else if (result.statusCode === 400) {
+      toast.error(
+        <ToastMessage title={t('validation:error')} message={result.message} />
+      );
+    } else {
+      toast.error(
+        <ToastMessage title={t('validation:error')} message={t('search:queryError')} />
+      );
+    }
   };
 
   return (
@@ -65,6 +78,7 @@ const BasicSearch = ({
               </EuiFlexItem>
             </EuiFlexGroup>
           </form>
+          <HowToBasicSearch />
         </EuiFlexItem>
       </EuiFlexGroup>
     </>
diff --git a/src/pages/search/BasicSearch/HowToBasicSearch.js b/src/pages/search/BasicSearch/HowToBasicSearch.js
new file mode 100644
index 0000000..1b4dd8a
--- /dev/null
+++ b/src/pages/search/BasicSearch/HowToBasicSearch.js
@@ -0,0 +1,120 @@
+import React from 'react';
+import {
+  EuiAccordion,
+  EuiButtonEmpty,
+  EuiFlexGrid,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiIcon,
+  EuiPanel,
+  EuiSpacer,
+  EuiText,
+  EuiTitle,
+  useGeneratedHtmlId,
+} from '@elastic/eui';
+import { Trans, useTranslation } from 'react-i18next';
+
+const HowToBasicSearch = () => {
+  const { t, ready } = useTranslation('search');
+
+  const Example = ({ query, desc }) => {
+    return (
+      <EuiFlexGroup>
+        <EuiText color={'success'}>
+          <code>
+            <EuiIcon type="search" /> <Trans i18nKey={query} />
+          </code>
+        </EuiText>
+        <EuiText>
+          <Trans i18nKey={desc} />
+        </EuiText>
+      </EuiFlexGroup>
+    );
+  };
+
+  const Examples = () => {
+    const examples = t('search:basicSearch.howTo.examples', { returnObjects: true });
+
+    return (
+      <EuiPanel hasShadow={false} hasBorder={true}>
+        <EuiTitle size={'xs'}>
+          <p>{t('search:basicSearch.howTo.examplesTitle')}</p>
+        </EuiTitle>
+        <EuiSpacer size={'s'} />
+        <EuiFlexGrid columns={1} gutterSize={'s'} direction={'column'}>
+          {examples.map((example, index) => {
+            return (
+              <EuiFlexItem key={index}>
+                <Example query={example.query} desc={example.desc} />
+              </EuiFlexItem>
+            );
+          })}
+        </EuiFlexGrid>
+      </EuiPanel>
+    );
+  };
+
+  const Section = ({ title, text }) => {
+    return (
+      <EuiPanel hasShadow={false} hasBorder={true}>
+        <EuiTitle size={'xs'}>
+          <p>{t(title)}</p>
+        </EuiTitle>
+        <EuiPanel hasShadow={false} paddingSize={'s'}>
+          <EuiText>
+            <ul>
+              {text.map((item, index) => {
+                return (
+                  <li key={index}>
+                    <Trans i18nKey={item} />
+                  </li>
+                );
+              })}
+            </ul>
+          </EuiText>
+        </EuiPanel>
+      </EuiPanel>
+    );
+  };
+
+  if (!ready) {
+    return 'Loading translations...';
+  }
+
+  const sections = t('search:basicSearch.howTo.sections', { returnObjects: true });
+
+  return (
+    <>
+      <EuiSpacer size="s" />
+      <EuiFlexGroup>
+        <EuiFlexItem>
+          <EuiAccordion
+            id={useGeneratedHtmlId({ prefix: 'howToAccordion' })}
+            buttonContent={
+              <EuiButtonEmpty>
+                {t('search:basicSearch.howTo.toggleAction')}
+              </EuiButtonEmpty>
+            }
+            buttonElement={'div'}
+          >
+            <EuiSpacer size="s" />
+            <Examples />
+            <EuiSpacer size="s" />
+            <EuiFlexGrid columns={2} gutterSize={'s'}>
+              {sections.map((section, index) => {
+                return (
+                  <EuiFlexItem key={index}>
+                    <Section title={section.title} text={section.content} />
+                  </EuiFlexItem>
+                );
+              })}
+            </EuiFlexGrid>
+            <EuiSpacer size="s" />
+          </EuiAccordion>
+        </EuiFlexItem>
+      </EuiFlexGroup>
+    </>
+  );
+};
+
+export default HowToBasicSearch;
-- 
GitLab


From d59173e110b22fc19e8775eb56ed4c8f930fae98 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Mon, 3 Feb 2025 10:12:25 +0100
Subject: [PATCH 2/3] [Profile] improved display

---
 .../profile/UserFieldsDisplaySettings.js      | 42 +++++++++----------
 1 file changed, 20 insertions(+), 22 deletions(-)

diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index e30e423..a043783 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -1,11 +1,11 @@
 import React, { useEffect, useState } from 'react';
 import {
-  EuiSpacer,
-  EuiSelectable,
   EuiButton,
-  EuiFlexItem,
   EuiFlexGroup,
+  EuiFlexItem,
   EuiPanel,
+  EuiSelectable,
+  EuiSpacer,
   EuiTitle,
 } from '@elastic/eui';
 import { Trans, useTranslation } from 'react-i18next';
@@ -131,25 +131,23 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
   const SelectableSettingsPanel = () => {
     return (
       <EuiFlexItem>
-        <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
-          <EuiSelectable
-            aria-label={t('profile:fieldsDisplaySettings.selectedOptionsLabel')}
-            options={settingsOptions}
-            onChange={(newOptions) => setSettingsOptions(newOptions)}
-            searchable
-            listProps={{ bordered: true }}
-            style={{ minHeight: '65vh' }}
-            height={'full'}
-          >
-            {(list, search) => (
-              <>
-                {search}
-                <EuiSpacer size={'xs'} />
-                {list}
-              </>
-            )}
-          </EuiSelectable>
-        </EuiPanel>
+        <EuiSelectable
+          aria-label={t('profile:fieldsDisplaySettings.selectedOptionsLabel')}
+          options={settingsOptions}
+          onChange={(newOptions) => setSettingsOptions(newOptions)}
+          searchable
+          listProps={{ bordered: true }}
+          style={{ minHeight: '65vh' }}
+          height={'full'}
+        >
+          {(list, search) => (
+            <>
+              {search}
+              <EuiSpacer size={'xs'} />
+              {list}
+            </>
+          )}
+        </EuiSelectable>
       </EuiFlexItem>
     );
   };
-- 
GitLab


From 14ed1d5897a79208a1c0972b576188016d8f08dd Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Mon, 3 Feb 2025 10:15:38 +0100
Subject: [PATCH 3/3] [Layout] improved pages display

---
 src/components/Layout/Layout.js | 2 +-
 src/components/Layout/styles.js | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js
index b28f490..b7d705a 100644
--- a/src/components/Layout/Layout.js
+++ b/src/components/Layout/Layout.js
@@ -26,7 +26,7 @@ const Layout = () => {
           />
           <Header />
           <EuiFlexGroup>
-            <EuiPageSection grow={true}>
+            <EuiPageSection grow={true} style={styles.pageContent}>
               <Outlet />
             </EuiPageSection>
           </EuiFlexGroup>
diff --git a/src/components/Layout/styles.js b/src/components/Layout/styles.js
index 5a2a1fe..fb86e31 100644
--- a/src/components/Layout/styles.js
+++ b/src/components/Layout/styles.js
@@ -3,6 +3,9 @@ const styles = {
     minHeight: '100vh',
     maxHeight: '100vh',
   },
+  pageContent: {
+    backgroundColor: '#ffffff',
+  },
 };
 
 export default styles;
-- 
GitLab