io-sanita-theme 2.18.2 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/locales/de/LC_MESSAGES/volto.po +10 -0
  3. package/locales/en/LC_MESSAGES/volto.po +10 -0
  4. package/locales/es/LC_MESSAGES/volto.po +10 -0
  5. package/locales/fr/LC_MESSAGES/volto.po +10 -0
  6. package/locales/it/LC_MESSAGES/volto.po +10 -0
  7. package/locales/volto.pot +11 -1
  8. package/package.json +1 -1
  9. package/src/components/Blocks/Listing/Table/TableTemplate.jsx +27 -8
  10. package/src/components/Blocks/Listing/Table/table-templates.scss +5 -0
  11. package/src/components/Blocks/QuickSearch/Body.jsx +8 -1
  12. package/src/components/OverlayLoading/OverlayLoading.jsx +18 -0
  13. package/src/components/OverlayLoading/overlayLoading.scss +12 -0
  14. package/src/components/View/Bando/Dates.jsx +41 -36
  15. package/src/components/index.js +2 -0
  16. package/src/components/layout/Header/HeaderSearch/SearchModal.jsx +7 -6
  17. package/src/components/layout/Header/HeaderSearch/searchModal.scss +0 -13
  18. package/src/config/blocks/index.js +2 -0
  19. package/src/customizations/volto/components/manage/Blocks/Search/SearchBlockView.jsx +50 -2
  20. package/src/customizations/volto/components/manage/Blocks/Search/components/SearchDetails.jsx +1 -1
  21. package/src/customizations/volto/components/manage/Blocks/Search/components/SelectFacet.jsx +2 -2
  22. package/src/customizations/volto/components/manage/Blocks/Search/components/SortOn.jsx +85 -0
  23. package/src/customizations/volto/components/manage/Blocks/Search/layout/LeftColumnFacets.jsx +33 -2
  24. package/src/customizations/volto/components/manage/Blocks/Search/layout/RightColumnFacets.jsx +37 -3
  25. package/src/customizations/volto/components/manage/Blocks/Search/schema.js +20 -0
  26. package/src/customizations/volto/components/theme/NotFound/NotFound.jsx +89 -7
  27. package/src/customizations/volto/helpers/Api/APIResourceWithAuth.js +43 -0
  28. package/src/overrideTranslations.jsx +9 -0
  29. package/src/theme/io-sanita/_main.scss +17 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.20.0](https://github.com/RedTurtle/io-sanita-theme/compare/2.19.0...2.20.0) (2025-08-20)
4
+
5
+ ### Features
6
+
7
+ * search table layout: sort / export csv+pdf ([#102](https://github.com/RedTurtle/io-sanita-theme/issues/102)) ([c0095b8](https://github.com/RedTurtle/io-sanita-theme/commit/c0095b8c416c9b00433d25b69a71080ba2046e22))
8
+ * Site Search bar into page 404 ([#103](https://github.com/RedTurtle/io-sanita-theme/issues/103)) ([b07b3e0](https://github.com/RedTurtle/io-sanita-theme/commit/b07b3e02f085eb0854f938a987465a1b7f9dd9ea))
9
+
10
+ ### Bug Fixes
11
+
12
+ * 404 on mobile ([3525b85](https://github.com/RedTurtle/io-sanita-theme/commit/3525b8520b6827634a2e9bf1b327d44c135827fd))
13
+ * added space before total results number in search results ([#101](https://github.com/RedTurtle/io-sanita-theme/issues/101)) ([ee9f384](https://github.com/RedTurtle/io-sanita-theme/commit/ee9f384439cfcf9598128cd3de1c0225cbf9f634))
14
+ * missing showDownloadActions option ([12aa80a](https://github.com/RedTurtle/io-sanita-theme/commit/12aa80a024bc2a910a8356f6b2774c21efd3c905))
15
+ * use SelectInput from iosanita-theme in Search SelectFacet component, instead of the one from design-react-kit ([#100](https://github.com/RedTurtle/io-sanita-theme/issues/100)) ([7c4b533](https://github.com/RedTurtle/io-sanita-theme/commit/7c4b5331ae09f662e7ed0fd38ea107cf308340c8))
16
+
17
+ ### Maintenance
18
+
19
+ * fix datatable schema ([1b63442](https://github.com/RedTurtle/io-sanita-theme/commit/1b63442cd8d5a18cda8cd7f21a6ce2172e158675))
20
+
21
+ ## [2.19.0](https://github.com/RedTurtle/io-sanita-theme/compare/2.18.2...2.19.0) (2025-07-08)
22
+
23
+ ### Features
24
+
25
+ * aggiunta la possibilittà di usare le date del bando anche al di fuori della componente di default, per personalizzarre etichette e valori ([5f497b6](https://github.com/RedTurtle/io-sanita-theme/commit/5f497b64173e51c5db7748dc55f299ad6a1e7cf5))
26
+
27
+ ### Bug Fixes
28
+
29
+ * in datatable rimuove ora, se non valorizzata ([1d14b88](https://github.com/RedTurtle/io-sanita-theme/commit/1d14b8886dcdc80efeb61362618f74e1b19d6d42))
30
+ * nella variannte tabella reinserito utilizzo di schema per le prroprietà dei campi, ad uso della redazione quando aggiunge nuovi campi ([1bd7083](https://github.com/RedTurtle/io-sanita-theme/commit/1bd7083be33b4c54e1021f19ce0f726b62b89863))
31
+
3
32
  ## [2.18.2](https://github.com/RedTurtle/io-sanita-theme/compare/2.18.1...2.18.2) (2025-07-03)
4
33
 
5
34
  ### Bug Fixes
@@ -896,6 +896,11 @@ msgstr ""
896
896
  msgid "Visible only in view mode"
897
897
  msgstr ""
898
898
 
899
+ #. Default: "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
900
+ #: overrideTranslations
901
+ msgid "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
902
+ msgstr ""
903
+
899
904
  #. Default: "You are trying to access a protected resource, please {login} first."
900
905
  #: components/Unauthorized/Unauthorized
901
906
  msgid "You are trying to access a protected resource, please {login} first."
@@ -2359,6 +2364,11 @@ msgstr ""
2359
2364
  msgid "open_end"
2360
2365
  msgstr ""
2361
2366
 
2367
+ #. Default: "or you can go to the"
2368
+ #: overrideTranslations
2369
+ msgid "or you can go to the "
2370
+ msgstr ""
2371
+
2362
2372
  #. Default: "Ordina per"
2363
2373
  #: components/Search/Search
2364
2374
  msgid "order_by"
@@ -891,6 +891,11 @@ msgstr "View all"
891
891
  msgid "Visible only in view mode"
892
892
  msgstr ""
893
893
 
894
+ #. Default: "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
895
+ #: overrideTranslations
896
+ msgid "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
897
+ msgstr ""
898
+
894
899
  #. Default: "You are trying to access a protected resource, please {login} first."
895
900
  #: components/Unauthorized/Unauthorized
896
901
  msgid "You are trying to access a protected resource, please {login} first."
@@ -2354,6 +2359,11 @@ msgstr "Open link in a new tab"
2354
2359
  msgid "open_end"
2355
2360
  msgstr "This event has an open/variable end date."
2356
2361
 
2362
+ #. Default: "or you can go to the"
2363
+ #: overrideTranslations
2364
+ msgid "or you can go to the "
2365
+ msgstr ""
2366
+
2357
2367
  #. Default: "Ordina per"
2358
2368
  #: components/Search/Search
2359
2369
  msgid "order_by"
@@ -898,6 +898,11 @@ msgstr "Ver todo"
898
898
  msgid "Visible only in view mode"
899
899
  msgstr ""
900
900
 
901
+ #. Default: "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
902
+ #: overrideTranslations
903
+ msgid "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
904
+ msgstr ""
905
+
901
906
  #. Default: "You are trying to access a protected resource, please {login} first."
902
907
  #: components/Unauthorized/Unauthorized
903
908
  msgid "You are trying to access a protected resource, please {login} first."
@@ -2361,6 +2366,11 @@ msgstr ""
2361
2366
  msgid "open_end"
2362
2367
  msgstr ""
2363
2368
 
2369
+ #. Default: "or you can go to the"
2370
+ #: overrideTranslations
2371
+ msgid "or you can go to the "
2372
+ msgstr ""
2373
+
2364
2374
  #. Default: "Ordina per"
2365
2375
  #: components/Search/Search
2366
2376
  msgid "order_by"
@@ -898,6 +898,11 @@ msgstr "Voir tout"
898
898
  msgid "Visible only in view mode"
899
899
  msgstr ""
900
900
 
901
+ #. Default: "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
902
+ #: overrideTranslations
903
+ msgid "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
904
+ msgstr ""
905
+
901
906
  #. Default: "You are trying to access a protected resource, please {login} first."
902
907
  #: components/Unauthorized/Unauthorized
903
908
  msgid "You are trying to access a protected resource, please {login} first."
@@ -2361,6 +2366,11 @@ msgstr ""
2361
2366
  msgid "open_end"
2362
2367
  msgstr ""
2363
2368
 
2369
+ #. Default: "or you can go to the"
2370
+ #: overrideTranslations
2371
+ msgid "or you can go to the "
2372
+ msgstr ""
2373
+
2364
2374
  #. Default: "Ordina per"
2365
2375
  #: components/Search/Search
2366
2376
  msgid "order_by"
@@ -891,6 +891,11 @@ msgstr "Vedi tutto"
891
891
  msgid "Visible only in view mode"
892
892
  msgstr "Visibile solo in modalità di view"
893
893
 
894
+ #. Default: "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
895
+ #: overrideTranslations
896
+ msgid "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
897
+ msgstr "Ci scusiamo per l'inconveniente, la pagina cui stai provando ad accedere non esiste a questo indirizzo. Puoi usare la barra di ricerca qui sotto per trovare quello che stavi cercando:"
898
+
894
899
  #. Default: "You are trying to access a protected resource, please {login} first."
895
900
  #: components/Unauthorized/Unauthorized
896
901
  msgid "You are trying to access a protected resource, please {login} first."
@@ -2354,6 +2359,11 @@ msgstr ""
2354
2359
  msgid "open_end"
2355
2360
  msgstr ""
2356
2361
 
2362
+ #. Default: "or you can go to the"
2363
+ #: overrideTranslations
2364
+ msgid "or you can go to the "
2365
+ msgstr "oppure vai alla "
2366
+
2357
2367
  #. Default: "Ordina per"
2358
2368
  #: components/Search/Search
2359
2369
  msgid "order_by"
package/locales/volto.pot CHANGED
@@ -1,7 +1,7 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Project-Id-Version: Plone\n"
4
- "POT-Creation-Date: 2025-06-24T13:43:29.010Z\n"
4
+ "POT-Creation-Date: 2025-08-13T12:41:17.004Z\n"
5
5
  "Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
6
6
  "Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
7
7
  "Content-Type: text/plain; charset=utf-8\n"
@@ -893,6 +893,11 @@ msgstr ""
893
893
  msgid "Visible only in view mode"
894
894
  msgstr ""
895
895
 
896
+ #. Default: "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
897
+ #: overrideTranslations
898
+ msgid "We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:"
899
+ msgstr ""
900
+
896
901
  #. Default: "You are trying to access a protected resource, please {login} first."
897
902
  #: components/Unauthorized/Unauthorized
898
903
  msgid "You are trying to access a protected resource, please {login} first."
@@ -2356,6 +2361,11 @@ msgstr ""
2356
2361
  msgid "open_end"
2357
2362
  msgstr ""
2358
2363
 
2364
+ #. Default: "or you can go to the"
2365
+ #: overrideTranslations
2366
+ msgid "or you can go to the "
2367
+ msgstr ""
2368
+
2359
2369
  #. Default: "Ordina per"
2360
2370
  #: components/Search/Search
2361
2371
  msgid "order_by"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "io-sanita-theme",
3
- "version": "2.18.2",
3
+ "version": "2.20.0",
4
4
  "description": "io-sanita-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
7
7
  import { useIntl, defineMessages } from 'react-intl';
8
8
  import { Row, Col, Table } from 'design-react-kit';
9
9
  import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
10
+ import { useSelector } from 'react-redux';
10
11
 
11
12
  import { ListingContainer } from 'io-sanita-theme/components/Blocks';
12
13
  import { LinkMore } from 'io-sanita-theme/components';
@@ -34,6 +35,9 @@ const TableTemplate = (props) => {
34
35
  const intl = useIntl();
35
36
  const { views } = config.widgets;
36
37
 
38
+ // necessario per gli edditor nel momento in cui aggiungono nuove colonne
39
+ const ct_schema = useSelector((state) => state.ct_schema?.subrequests);
40
+
37
41
  let render_columns =
38
42
  (columns ?? []).filter((c) => c.field === 'title').length > 0
39
43
  ? columns
@@ -49,7 +53,10 @@ const TableTemplate = (props) => {
49
53
  <thead className="table-light">
50
54
  <tr>
51
55
  {render_columns.map((c, index) => {
52
- const field_properties = c.field_properties ?? {};
56
+ const field_properties =
57
+ c.field_properties ??
58
+ ct_schema?.[c.ct]?.result?.properties?.[c.field] ??
59
+ {};
53
60
 
54
61
  return (
55
62
  <th
@@ -71,32 +78,44 @@ const TableTemplate = (props) => {
71
78
  {items.map((item, index) => (
72
79
  <tr key={index}>
73
80
  {render_columns.map((c, index) => {
74
- const field_properties = c.field_properties ?? {};
81
+ const field_properties =
82
+ c.field_properties ??
83
+ ct_schema?.[c.ct]?.result?.properties?.[c.field] ??
84
+ {};
75
85
  let render_value = JSON.stringify(item[c.field]);
76
86
 
77
87
  if (field_properties) {
78
- let field = {
88
+ const field = {
79
89
  ...field_properties,
80
90
  id: c.field,
81
91
  widget: getWidget(c.field, field_properties),
82
92
  };
93
+ const Widget = views?.getWidget(field);
83
94
 
84
- let Widget = views?.getWidget(field);
85
-
86
- let widget_props = {
95
+ const widget_props = {
87
96
  behavior: field_properties.behavior,
88
97
  };
98
+ if (field_properties.widget === 'datetime') {
99
+ widget_props.format = 'DD/MM/yyyy HH:MM';
100
+ }
101
+ // per questi campi si è deciso dii non pubblicare ora:minuti
89
102
  switch (c.field) {
90
103
  case 'apertura_bando':
91
104
  case 'chiusura_procedimento_bando':
92
105
  case 'scadenza_domande_bando':
93
106
  case 'scadenza_bando':
94
- widget_props.format = 'DD MMM yyyy';
107
+ widget_props.format = 'DD/MM/yyyy';
95
108
  break;
96
109
  default:
97
110
  break;
98
111
  }
99
-
112
+ // rimuove ora, se non valorizzata
113
+ if (
114
+ field_properties.widget === 'datetime' &&
115
+ item[c.field]?.indexOf('T00:00') > 0
116
+ ) {
117
+ widget_props.format = 'DD/MM/yyyy';
118
+ }
100
119
  if (field_properties.vocabulary) {
101
120
  widget_props.vocabulary =
102
121
  field_properties.vocabulary['@id'];
@@ -1,3 +1,8 @@
1
+ // XXX: workaround per template search con tabella e fltri con faccette laterali in edit
2
+ // body.cms-ui.has-toolbar.has-sidebar .public-ui .table-template .px-0.container {
3
+ // width: 100% !important;
4
+ // }
5
+
1
6
  .table-template {
2
7
  table.table {
3
8
  th {
@@ -3,7 +3,11 @@ import cx from 'classnames';
3
3
  import { useIntl, defineMessages } from 'react-intl';
4
4
  import { useSelector } from 'react-redux';
5
5
  import { Container, Row, Col, Button } from 'design-react-kit';
6
- import { SearchBar, QuickSearch } from 'io-sanita-theme/components';
6
+ import {
7
+ SearchBar,
8
+ QuickSearch,
9
+ OverlayLoading,
10
+ } from 'io-sanita-theme/components';
7
11
  import { SearchUtils } from 'io-sanita-theme/helpers';
8
12
  import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
9
13
  import './quickSearchBlock.scss';
@@ -19,9 +23,11 @@ const Body = ({ data, id, isEditMode }) => {
19
23
  const intl = useIntl();
20
24
  const [searchableText, setSearchableText] = useState();
21
25
  const subsite = useSelector((state) => state.subsite?.data);
26
+ const [redirectingToResults, setRedirectingToResults] = useState(false);
22
27
 
23
28
  useEffect(() => {
24
29
  if (searchableText?.length > 0) {
30
+ setRedirectingToResults(true);
25
31
  window.location.href =
26
32
  window.location.origin +
27
33
  SearchUtils.getSearchParamsURL({
@@ -86,6 +92,7 @@ const Body = ({ data, id, isEditMode }) => {
86
92
  </div>
87
93
  )}
88
94
  </Container>
95
+ <OverlayLoading loading={redirectingToResults} />
89
96
  </div>
90
97
  );
91
98
  };
@@ -0,0 +1,18 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ import React from 'react';
3
+
4
+ import { Spinner } from 'design-react-kit';
5
+
6
+ import './overlayLoading.scss';
7
+
8
+ const OverlayLoading = ({ loading }) => {
9
+ return loading ? (
10
+ <div className="overlay loading-results">
11
+ <Spinner active />
12
+ </div>
13
+ ) : (
14
+ <></>
15
+ );
16
+ };
17
+
18
+ export default OverlayLoading;
@@ -0,0 +1,12 @@
1
+ .overlay.loading-results {
2
+ width: 100%;
3
+ height: 100%;
4
+ position: fixed;
5
+ z-index: 9999;
6
+ top: 0;
7
+ left: 0;
8
+ background-color: hsl(0deg 0% 100% / 64%);
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ }
@@ -84,44 +84,49 @@ const BandoDates = ({ content }) => {
84
84
  ];
85
85
 
86
86
  dates.sort((a, b) => a.date - b.date);
87
- return content ? (
88
- <div className="point-list-wrapper my-4 mb-5">
89
- {dates.map((item, index) => {
90
- return (
91
- item.date && (
92
- <div className="point-list" key={index}>
93
- <div
94
- className="point-list-aside point-list-warning"
95
- aria-label={item.date.format('DD MMMM Y')}
87
+
88
+ return content ? <Dates dates={dates} /> : null;
89
+ };
90
+
91
+ const Dates = ({ dates }) => (
92
+ <div className="point-list-wrapper my-4 mb-5">
93
+ {dates.map((item, index) => {
94
+ return (
95
+ item.date && (
96
+ <div className="point-list" key={index}>
97
+ <div
98
+ className="point-list-aside point-list-warning"
99
+ aria-label={item.date.format('DD MMMM Y')}
100
+ >
101
+ <span className="point-date text-monospace" aria-hidden={true}>
102
+ {item.date.format('DD')}
103
+ </span>
104
+ <span className="point-month text-monospace" aria-hidden={true}>
105
+ {item.date.format('MMM')}/{item.date.format('YY')}
106
+ </span>
107
+ </div>
108
+ <div className="point-list-content">
109
+ <Card
110
+ className="card card-teaser rounded shadow"
111
+ noWrapper={true}
112
+ tag="div"
96
113
  >
97
- <span className="point-date text-monospace" aria-hidden={true}>
98
- {item.date.format('DD')}
99
- </span>
100
- <span className="point-month text-monospace" aria-hidden={true}>
101
- {item.date.format('MMM')}/{item.date.format('YY')}
102
- </span>
103
- </div>
104
- <div className="point-list-content">
105
- <Card
106
- className="card card-teaser rounded shadow"
107
- noWrapper={true}
108
- tag="div"
109
- >
110
- <CardBody tag="div" className={'card-body'}>
111
- <CardTitle tag="p">
112
- {item.show_hour && <>{item.date.format('HH:mm')} - </>}
113
- {item.label}
114
- </CardTitle>
115
- </CardBody>
116
- </Card>
117
- </div>
114
+ <CardBody tag="div" className={'card-body'}>
115
+ <CardTitle tag="p">
116
+ {item.show_hour && <>{item.date.format('HH:mm')} - </>}
117
+ {item.label}
118
+ </CardTitle>
119
+ </CardBody>
120
+ </Card>
118
121
  </div>
119
- )
120
- );
121
- })}
122
- </div>
123
- ) : null;
124
- };
122
+ </div>
123
+ )
124
+ );
125
+ })}
126
+ </div>
127
+ );
128
+
129
+ export { Dates };
125
130
 
126
131
  export default BandoDates;
127
132
 
@@ -1,4 +1,5 @@
1
1
  import loadable from '@loadable/component';
2
+ import OverlayLoading from './OverlayLoading/OverlayLoading';
2
3
 
3
4
  export Icon from 'io-sanita-theme/components/Icon/Icon';
4
5
  export FontAwesomeIcon from 'io-sanita-theme/components/Icon/FontAwesomeIcon';
@@ -30,6 +31,7 @@ export HeaderSlim from 'io-sanita-theme/components/layout/Header/HeaderSlim/Head
30
31
  export HeaderContacts from 'io-sanita-theme/components/layout/Header/HeaderContacts/HeaderContacts';
31
32
  export HeaderCenter from 'io-sanita-theme/components/layout/Header/HeaderCenter';
32
33
  export SubsiteHeader from 'io-sanita-theme/components/layout/Header/SubsiteHeader/SubsiteHeader';
34
+ export OverlayLoading from 'io-sanita-theme/components/OverlayLoading/OverlayLoading';
33
35
  export SearchModal from 'io-sanita-theme/components/layout/Header/HeaderSearch/SearchModal';
34
36
  export UserLoggedMenu from 'io-sanita-theme/components/layout/Header/HeaderSlim/UserLoggedMenu';
35
37
  export LoginButton from 'io-sanita-theme/components/layout/Header/HeaderSlim/LoginButton';
@@ -15,7 +15,12 @@ import {
15
15
  Spinner,
16
16
  } from 'design-react-kit';
17
17
 
18
- import { SearchBar, QuickSearch, Icon } from 'io-sanita-theme/components';
18
+ import {
19
+ SearchBar,
20
+ QuickSearch,
21
+ Icon,
22
+ OverlayLoading,
23
+ } from 'io-sanita-theme/components';
19
24
  import { SearchUtils } from 'io-sanita-theme/helpers';
20
25
 
21
26
  import './searchModal.scss';
@@ -141,11 +146,7 @@ const SearchModal = ({ closeModal, show }) => {
141
146
  />
142
147
  </div>
143
148
  </Container>
144
- {redirectingToResults && (
145
- <div className="overlay loading-results">
146
- <Spinner active />
147
- </div>
148
- )}
149
+ <OverlayLoading loading={redirectingToResults} />
149
150
  </ModalBody>
150
151
  </Modal>
151
152
  );
@@ -43,19 +43,6 @@ body.search-modal-opened {
43
43
  margin-left: auto;
44
44
  }
45
45
 
46
- .overlay.loading-results {
47
- width: 100%;
48
- height: 100%;
49
- position: fixed;
50
- z-index: 9999;
51
- top: 0;
52
- left: 0;
53
- background-color: hsl(0deg 0% 100% / 64%);
54
- display: flex;
55
- align-items: center;
56
- justify-content: center;
57
- }
58
-
59
46
  @media (max-width: #{map-get($grid-breakpoints, lg)}) {
60
47
  .quick-search {
61
48
  h2.h6,
@@ -287,6 +287,8 @@ export const applyIoSanitaBlocksConfig = (config) => {
287
287
  },
288
288
  search: {
289
289
  ...config.blocks.blocksConfig.search,
290
+ // filtro top non personalizzato / non funzionante con layout agid
291
+ variations: config.blocks.blocksConfig.search.variations.filter((v) => v.id !== 'facetsTopSide'),
290
292
  templates: [
291
293
  ...listingVariations.map((v) => v.id).filter((v) => v !== 'carousel'),
292
294
  ],
@@ -4,7 +4,7 @@
4
4
  existing listing template styles
5
5
  */
6
6
 
7
- import React from 'react';
7
+ import React, { useState, useEffect } from 'react';
8
8
 
9
9
  import ListingBody from '@plone/volto/components/manage/Blocks/Listing/ListingBody';
10
10
  import { withBlockExtensions } from '@plone/volto/helpers/Extensions';
@@ -19,6 +19,14 @@ import { compose } from 'redux';
19
19
  import { useSelector } from 'react-redux';
20
20
  import isEqual from 'lodash/isEqual';
21
21
  import isFunction from 'lodash/isFunction';
22
+ import { useIntl, defineMessages } from 'react-intl';
23
+
24
+ const messages = defineMessages({
25
+ downloadInFormat: {
26
+ id: 'downloadInFormat',
27
+ defaultMessage: 'download in formato',
28
+ },
29
+ });
22
30
 
23
31
  const getListingBodyVariation = (data) => {
24
32
  const { variations } = config.blocks.blocksConfig.listing;
@@ -67,8 +75,10 @@ const applyDefaults = (data, root) => {
67
75
  };
68
76
 
69
77
  const SearchBlockView = (props) => {
70
- const { data, searchData, mode = 'view', variation } = props;
78
+ // console.log(props);
79
+ const { data, searchData, mode = 'view', variation, path, id } = props;
71
80
 
81
+ const intl = useIntl();
72
82
  const Layout =
73
83
  variation?.view ||
74
84
  config.blocks.blocksConfig.search.variations.find(
@@ -93,6 +103,22 @@ const SearchBlockView = (props) => {
93
103
 
94
104
  const { variations } = config.blocks.blocksConfig.listing;
95
105
  const listingBodyVariation = variations.find(({ id }) => id === selectedView);
106
+
107
+ const [downloadUrl, setDownloadUrl] = useState('');
108
+
109
+ useEffect(
110
+ () =>
111
+ setDownloadUrl(
112
+ `${path}/searchblock/@@download/${id}.__FORMAT__?${new URLSearchParams({
113
+ ...props.facets,
114
+ search: props.searchText,
115
+ sort_on: props.sortOn,
116
+ sort_order: props.sortOrder,
117
+ })}`,
118
+ ),
119
+ [props.facets, props.sortOn, props.sortOrder, props.searchText],
120
+ );
121
+
96
122
  if (!Layout) return null;
97
123
 
98
124
  return (
@@ -111,6 +137,28 @@ const SearchBlockView = (props) => {
111
137
  path={props.path}
112
138
  isEditMode={mode === 'edit'}
113
139
  />
140
+ {downloadUrl && data?.showDownloadActions && (
141
+ <div class="text-right" style={{textAlign: 'right'}}>
142
+ {intl.formatMessage(messages.downloadInFormat)}:{' '}
143
+ <a
144
+ href={downloadUrl.replace('__FORMAT__', 'csv')}
145
+ download
146
+ className="btn btn-xs btn-primary inline-link"
147
+ disabled={!downloadUrl}
148
+ >
149
+ CSV
150
+ </a>{' '}
151
+ <a
152
+ href={downloadUrl.replace('__FORMAT__', 'pdf')}
153
+ download
154
+ className="btn btn-xs btn-primary inline-link"
155
+ disabled={!downloadUrl}
156
+ >
157
+ PDF
158
+ </a>
159
+ {/* <a href={downloadUrl.replace('__FORMAT__', 'html')} className="btn btn-xs btn-primary inline-link">HTML</a> */}
160
+ </div>
161
+ )}
114
162
  </div>
115
163
  </Layout>
116
164
  </div>
@@ -24,7 +24,7 @@ const SearchDetails = ({ total, text, as = 'p', data }) => {
24
24
  {intl.formatMessage(commonSearchBlockMessages.searchedFor, {
25
25
  em: (...chunks) => <em>{chunks}</em>,
26
26
  searchedtext: text,
27
- })}
27
+ })}{' '}
28
28
  </>
29
29
  )}
30
30
  {data.showTotalResults && (
@@ -7,7 +7,7 @@ import {
7
7
  selectFacetStateToValue,
8
8
  selectFacetValueToQuery,
9
9
  } from '@plone/volto/components/manage/Blocks/Search/components/base';
10
- import { Select } from 'design-react-kit';
10
+ import { SelectInput } from 'io-sanita-theme/components';
11
11
 
12
12
  const SelectFacet = (props) => {
13
13
  const { facet, choices, isMulti, onChange, value, isEditMode } = props;
@@ -23,7 +23,7 @@ const SelectFacet = (props) => {
23
23
  {facet?.title || facet?.field?.label || ''}
24
24
  </label> */}
25
25
  {/* Cannot style with props because the kit is... the kit. Resorting to div[class*='-ValueContainer'] */}
26
- <Select
26
+ <SelectInput
27
27
  placeholder={facet?.title ?? (facet?.field?.label || 'select...')}
28
28
  aria-label={facet?.title ?? (facet?.field?.label || 'select...')}
29
29
  id={facet['@id']}
@@ -0,0 +1,85 @@
1
+ /* CUSTOMIZATIONS:
2
+ - Agid styling
3
+ */
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+ // import upSVG from '@plone/volto/icons/sort-up.svg';
6
+ // import downSVG from '@plone/volto/icons/sort-down.svg';
7
+ // import { Container, Row, Col, Icon } from 'design-react-kit';
8
+ import { SelectInput } from 'io-sanita-theme/components';
9
+
10
+ const messages = defineMessages({
11
+ noSelection: {
12
+ id: 'No selection',
13
+ defaultMessage: 'No selection',
14
+ },
15
+ sortOn: {
16
+ id: 'Sort on',
17
+ defaultMessage: 'Sort on',
18
+ },
19
+ ascending: {
20
+ id: 'Ascending',
21
+ defaultMessage: 'Ascending',
22
+ },
23
+ descending: {
24
+ id: 'Descending',
25
+ defaultMessage: 'Descending',
26
+ },
27
+ sortedOn: {
28
+ id: 'Sorted on',
29
+ defaultMessage: 'Sorted on',
30
+ },
31
+ });
32
+
33
+ const SortOn = (props) => {
34
+ const {
35
+ data = {},
36
+ sortOn = null,
37
+ sortOrder = null,
38
+ setSortOn,
39
+ setSortOrder,
40
+ isEditMode,
41
+ querystring = {},
42
+ } = props;
43
+
44
+ const intl = useIntl();
45
+ const sortableOptions = data?.columns
46
+ ? [
47
+ { value: 'sortable_title', label: 'Titolo' },
48
+ ...data?.columns?.map((f) => {
49
+ return { value: f.field, label: f.title };
50
+ }),
51
+ ].filter((o) => querystring?.['indexes']?.[o.value]?.sortable)
52
+ : [{ value: 'sortable_title', label: 'Titolo' }];
53
+
54
+ const sortOrderOptions = [
55
+ { value: 'ascending', label: intl.formatMessage(messages.ascending) },
56
+ { value: 'descending', label: intl.formatMessage(messages.descending) },
57
+ ];
58
+
59
+ return (
60
+ <div className="pt-4">
61
+ <h6>{intl.formatMessage(messages.sortOn)}</h6>
62
+ {/* <pre>{JSON.stringify(searchData, null,2)}</pre> */}
63
+ {/* <pre>{JSON.stringify(data.columns, null,2)}</pre> */}
64
+ {/* <pre>{sortOn} {sortOrder}</pre> */}
65
+ <div className="pt-2">
66
+ <SelectInput
67
+ id="sortOn"
68
+ value={sortableOptions.find((o) => o.value === sortOn)}
69
+ onChange={(opt) => setSortOn(opt.value)}
70
+ options={sortableOptions}
71
+ />
72
+ </div>
73
+ <div className="pt-2">
74
+ <SelectInput
75
+ id="sortOrder"
76
+ value={sortOrderOptions.find((o) => o.value === sortOrder)}
77
+ onChange={(opt) => setSortOrder(opt.value)}
78
+ options={sortOrderOptions}
79
+ />
80
+ </div>
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default SortOn;
@@ -7,6 +7,7 @@ import {
7
7
  SearchDetails,
8
8
  Facets,
9
9
  FilterList,
10
+ SortOn,
10
11
  } from '@plone/volto/components/manage/Blocks/Search/components';
11
12
  import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
12
13
  import { Container, Row, Col, Icon } from 'design-react-kit';
@@ -25,6 +26,8 @@ const LeftColumnFacets = (props) => {
25
26
  totalItems,
26
27
  facets,
27
28
  setFacets,
29
+ sortOn,
30
+ sortOrder,
28
31
  onTriggerSearch,
29
32
  searchedText, // search text for previous search
30
33
  isEditMode,
@@ -35,10 +38,10 @@ const LeftColumnFacets = (props) => {
35
38
  } = props;
36
39
  const { showSearchButton } = data;
37
40
  const isLive = !showSearchButton;
38
- const showColumn =
41
+ const showColumn = !isEditMode && (
39
42
  data.columnTextTitle ||
40
43
  richTextHasContent(data.columnText) ||
41
- data?.facets?.length > 0;
44
+ data?.facets?.length > 0);
42
45
  return (
43
46
  <div className="full-width bg-primary-lightest">
44
47
  <Container
@@ -95,6 +98,34 @@ const LeftColumnFacets = (props) => {
95
98
  />
96
99
  </div>
97
100
  )}
101
+ <div className="sort-views-wrapper">
102
+ {data.showSortOn && (
103
+ <SortOn
104
+ data={data}
105
+ querystring={querystring}
106
+ isEditMode={isEditMode}
107
+ sortOrder={sortOrder}
108
+ sortOn={sortOn}
109
+ setSortOn={(sortOn) => {
110
+ flushSync(() => {
111
+ // setSortOn(sortOn);
112
+ onTriggerSearch(searchedText || '', facets, sortOn);
113
+ });
114
+ }}
115
+ setSortOrder={(sortOrder) => {
116
+ flushSync(() => {
117
+ // setSortOrder(sortOrder);
118
+ onTriggerSearch(
119
+ searchedText || '',
120
+ facets,
121
+ sortOn,
122
+ sortOrder,
123
+ );
124
+ });
125
+ }}
126
+ />
127
+ )}
128
+ </div>
98
129
  </div>
99
130
  )}
100
131
 
@@ -1,12 +1,12 @@
1
1
  /* CUSTOMIZATIONS:
2
2
  - Agid styling
3
3
  */
4
- import React from 'react';
5
4
  import {
6
5
  SearchInput,
7
6
  SearchDetails,
8
7
  Facets,
9
8
  FilterList,
9
+ SortOn,
10
10
  } from '@plone/volto/components/manage/Blocks/Search/components';
11
11
  import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
12
12
  import { Container, Row, Col, Icon } from 'design-react-kit';
@@ -26,6 +26,8 @@ const RightColumnFacets = (props) => {
26
26
  totalItems,
27
27
  facets,
28
28
  setFacets,
29
+ sortOn,
30
+ sortOrder,
29
31
  onTriggerSearch,
30
32
  searchedText, // search text for previous search
31
33
  isEditMode,
@@ -36,10 +38,13 @@ const RightColumnFacets = (props) => {
36
38
  } = props;
37
39
  const { showSearchButton } = data;
38
40
  const isLive = !showSearchButton;
39
- const showColumn =
41
+
42
+ const showColumn = !isEditMode && (
40
43
  data.columnTextTitle ||
41
44
  richTextHasContent(data.columnText) ||
42
- data?.facets?.length > 0;
45
+ data?.facets?.length > 0 ||
46
+ data?.showOrderOptions);
47
+
43
48
  return (
44
49
  <div className="full-width bg-primary-lightest">
45
50
  <Container className="searchBlock-facets right-column-facets" stackable>
@@ -125,6 +130,35 @@ const RightColumnFacets = (props) => {
125
130
  />
126
131
  </div>
127
132
  )}
133
+
134
+ <div className="sort-views-wrapper">
135
+ {data.showSortOn && (
136
+ <SortOn
137
+ data={data}
138
+ querystring={querystring}
139
+ isEditMode={isEditMode}
140
+ sortOrder={sortOrder}
141
+ sortOn={sortOn}
142
+ setSortOn={(sortOn) => {
143
+ flushSync(() => {
144
+ // setSortOn(sortOn);
145
+ onTriggerSearch(searchedText || '', facets, sortOn);
146
+ });
147
+ }}
148
+ setSortOrder={(sortOrder) => {
149
+ flushSync(() => {
150
+ // setSortOrder(sortOrder);
151
+ onTriggerSearch(
152
+ searchedText || '',
153
+ facets,
154
+ sortOn,
155
+ sortOrder,
156
+ );
157
+ });
158
+ }}
159
+ />
160
+ )}
161
+ </div>
128
162
  </div>
129
163
  )}
130
164
  </Row>
@@ -123,6 +123,14 @@ const messages = defineMessages({
123
123
  id: 'Show total results',
124
124
  defaultMessage: 'Show total results',
125
125
  },
126
+ showSortOn: {
127
+ id: 'Show sorting?',
128
+ defaultMessage: 'Show sorting?',
129
+ },
130
+ showDownloadActions: {
131
+ id: 'showDownloadActions',
132
+ defaultMessage: 'Mostra azioni downlad CSV/PDF',
133
+ },
126
134
  columnTextTitle: {
127
135
  id: 'columnTextTitle',
128
136
  defaultMessage: 'Intestazione della colonna',
@@ -282,6 +290,8 @@ const SearchSchema = ({ data = {}, intl }) => {
282
290
  // ...(data.showSearchInput ? ['searchInputPrompt'] : []),
283
291
  // ...(data.showSearchButton ? ['searchButtonLabel'] : []),
284
292
  'showTotalResults',
293
+ 'showSortOn',
294
+ 'showDownloadActions',
285
295
  ],
286
296
  },
287
297
  ],
@@ -307,6 +317,16 @@ const SearchSchema = ({ data = {}, intl }) => {
307
317
  title: intl.formatMessage(messages.showTotalResults),
308
318
  default: true,
309
319
  },
320
+ showSortOn: {
321
+ type: 'boolean',
322
+ title: intl.formatMessage(messages.showSortOn),
323
+ default: false,
324
+ },
325
+ showDownloadActions: {
326
+ type: 'boolean',
327
+ title: intl.formatMessage(messages.showDownloadActions),
328
+ default: true,
329
+ },
310
330
  searchButtonLabel: {
311
331
  title: intl.formatMessage(messages.searchButtonLabel),
312
332
  placeholder: intl.formatMessage(messages.searchButtonPlaceholder),
@@ -1,12 +1,16 @@
1
1
  /*
2
2
  CUSTOMIZATIONS:
3
3
  - Removed the "Site Administration" link, added a link to the home page
4
+ - Added a Search in site bar
4
5
  */
5
6
 
6
- import { useEffect } from 'react';
7
+ import { useEffect, useState, useRef } from 'react';
7
8
 
8
9
  import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
9
10
  import { FormattedMessage } from 'react-intl';
11
+ import { defineMessages, useIntl } from 'react-intl';
12
+ import { useLocation } from 'react-router-dom';
13
+ import qs from 'query-string';
10
14
  import { Link } from 'react-router-dom';
11
15
  import { Container } from 'semantic-ui-react';
12
16
  import {
@@ -16,15 +20,55 @@ import {
16
20
  import { useDispatch, useSelector } from 'react-redux';
17
21
  import { getNavigation } from '@plone/volto/actions/navigation/navigation';
18
22
  import config from '@plone/volto/registry';
23
+ import { SearchBar, OverlayLoading } from 'io-sanita-theme/components';
24
+ import { SearchUtils } from 'io-sanita-theme/helpers';
25
+
26
+ import { Spinner } from 'design-react-kit';
19
27
 
20
28
  /**
21
29
  * Not found function.
22
30
  * @function NotFound
23
31
  * @returns {string} Markup of the not found page.
24
32
  */
33
+
34
+ const { getSearchParamsURL } = SearchUtils;
35
+
36
+ const messages = defineMessages({
37
+ closeSearch: {
38
+ id: 'closeSearch',
39
+ defaultMessage: 'Chiudi cerca',
40
+ },
41
+ closeSearchBack: {
42
+ id: 'closeSearchBack',
43
+ defaultMessage: 'Indietro',
44
+ },
45
+ search: {
46
+ id: 'search',
47
+ defaultMessage: 'Cerca',
48
+ },
49
+ searchLabel: {
50
+ id: 'searchLabel',
51
+ defaultMessage: 'Cerca nel sito',
52
+ },
53
+ error404maintext: {
54
+ id: 'We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:',
55
+ defaultMessage:
56
+ 'We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:',
57
+ },
58
+ error404hplink: {
59
+ id: 'or you can go to the ',
60
+ defaultMessage: 'or you can go to the ',
61
+ },
62
+ });
63
+
25
64
  const NotFound = () => {
26
65
  const dispatch = useDispatch();
27
66
  const lang = useSelector((state) => state.intl.locale);
67
+ const intl = useIntl();
68
+ const location = useLocation();
69
+ const inputRef = useRef(null);
70
+ const [redirectingToResults, setRedirectingToResults] = useState(false);
71
+ const subsite = useSelector((state) => state.subsite?.data);
28
72
 
29
73
  const navigationRootPath = config.settings.isMultilingual
30
74
  ? `/${toBackendLang(lang)}`
@@ -34,8 +78,25 @@ const NotFound = () => {
34
78
  dispatch(getNavigation(navigationRootPath, config.settings.navDepth));
35
79
  }, [dispatch, lang, navigationRootPath]);
36
80
 
81
+ const [searchableText, setSearchableText] = useState(
82
+ qs.parse(location.search)?.SearchableText ?? '',
83
+ );
84
+
85
+ const submitSearch = (_searchableText) => {
86
+ if (__CLIENT__) {
87
+ setRedirectingToResults(true);
88
+ window.location.href =
89
+ window.location.origin +
90
+ getSearchParamsURL({
91
+ searchableText: _searchableText ?? searchableText,
92
+ subsite,
93
+ currentLang: intl.locale,
94
+ });
95
+ }
96
+ };
97
+
37
98
  return (
38
- <Container className="view-wrapper">
99
+ <Container className="view-wrapper px-5 text-center py-3">
39
100
  <BodyClass className="page-not-found" />
40
101
  <h1>
41
102
  <FormattedMessage
@@ -43,16 +104,37 @@ const NotFound = () => {
43
104
  defaultMessage="This page does not seem to exist…"
44
105
  />
45
106
  </h1>
46
- <p className="description">
47
- <FormattedMessage
48
- id="We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the links below to help you find what you are looking for."
49
- defaultMessage="We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the links below to help you find what you are looking for."
50
- />
107
+ <p className="description text-center mt-3">
108
+ {intl.formatMessage(messages.error404maintext)}
51
109
  </p>
110
+ <Container className="search-bar-container my-5">
111
+ <div
112
+ className="search-filters"
113
+ role="search"
114
+ aria-label={intl.formatMessage(messages.searchLabel)}
115
+ >
116
+ <div className="mb-4">
117
+ <SearchBar
118
+ id="search-site-modal"
119
+ value={searchableText}
120
+ onChange={(v) => {
121
+ setSearchableText(v);
122
+ submitSearch(v);
123
+ }}
124
+ showSubmit={true}
125
+ ref={inputRef}
126
+ />
127
+ </div>
128
+ </div>
129
+ <OverlayLoading loading={redirectingToResults} />
130
+ </Container>
131
+
52
132
  <p>
133
+ {intl.formatMessage(messages.error404hplink)}
53
134
  <Link to={navigationRootPath}>
54
135
  <FormattedMessage id="Home page" defaultMessage="Home page" />
55
136
  </Link>
137
+ .
56
138
  </p>
57
139
  {/* <p>
58
140
  <FormattedMessage
@@ -0,0 +1,43 @@
1
+ /**
2
+ CUSTOMIZATIONS:
3
+ * Original from @plone/volto 18.9.1
4
+ *
5
+ * - querystring parameters are passed to the backend
6
+ *
7
+ */
8
+
9
+ import superagent from 'superagent';
10
+ import config from '@plone/volto/registry';
11
+ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
12
+
13
+ /**
14
+ * Get a resource image/file with authenticated (if token exist) API headers
15
+ * @function getAPIResourceWithAuth
16
+ * @param {Object} req Request object
17
+ * @return {string} The response with the image
18
+ */
19
+ export const getAPIResourceWithAuth = (req) =>
20
+ new Promise((resolve, reject) => {
21
+ const { settings } = config;
22
+ const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
23
+
24
+ let apiPath = '';
25
+ if (settings.internalApiPath && __SERVER__) {
26
+ apiPath = settings.internalApiPath;
27
+ } else if (__DEVELOPMENT__ && settings.devProxyToApiPath) {
28
+ apiPath = settings.devProxyToApiPath;
29
+ } else {
30
+ apiPath = settings.apiPath;
31
+ }
32
+ const request = superagent
33
+ .get(`${apiPath}${__DEVELOPMENT__ ? '' : APISUFIX}${req.path}`)
34
+ .query(req.query)
35
+ .maxResponseSize(settings.maxResponseSize)
36
+ .responseType('blob');
37
+ const authToken = req.universalCookies.get('auth_token');
38
+ if (authToken) {
39
+ request.set('Authorization', `Bearer ${authToken}`);
40
+ }
41
+ request.use(addHeadersFactory(req));
42
+ request.then(resolve).catch(reject);
43
+ });
@@ -178,4 +178,13 @@ defineMessages({
178
178
  id: 'descendingTableSort',
179
179
  defaultMessage: 'descending',
180
180
  },
181
+ error404maintext: {
182
+ id: 'We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:',
183
+ defaultMessage:
184
+ 'We apologize for the inconvenience, but the page you were trying to access is not at this address. You can use the search below to help you find what you are looking for:',
185
+ },
186
+ error404hplink: {
187
+ id: 'or you can go to the ',
188
+ defaultMessage: 'or you can go to the ',
189
+ },
181
190
  });
@@ -301,3 +301,20 @@ picture.volto-image.responsive img.full-width,
301
301
  margin: 0;
302
302
  box-shadow: none;
303
303
  }
304
+
305
+ //error 404
306
+ body.page-not-found {
307
+ .search-bar-container {
308
+ position: relative;
309
+ }
310
+
311
+ #customer-satisfaction-form {
312
+ display: none;
313
+ }
314
+
315
+ @media (min-width: #{map-get($grid-breakpoints, lg)}) {
316
+ .view-wrapper {
317
+ max-width: 50vw;
318
+ }
319
+ }
320
+ }