io-sanita-theme 2.22.1 → 2.23.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.23.1](https://github.com/RedTurtle/io-sanita-theme/compare/2.23.0...2.23.1) (2026-03-20)
4
+
5
+ ### Bug Fixes
6
+
7
+ * alert block height in view mode ([#120](https://github.com/RedTurtle/io-sanita-theme/issues/120)) ([58c969e](https://github.com/RedTurtle/io-sanita-theme/commit/58c969eda0d02e76b6535863eda7230db2808b14))
8
+ * uridecode filename ([#119](https://github.com/RedTurtle/io-sanita-theme/issues/119)) ([eea9e68](https://github.com/RedTurtle/io-sanita-theme/commit/eea9e681fc78a29ee50167191e1f558984da5bda))
9
+
10
+ ### Changes
11
+
12
+ * make codebase more flexible for addons ([#116](https://github.com/RedTurtle/io-sanita-theme/issues/116)) ([3e1b034](https://github.com/RedTurtle/io-sanita-theme/commit/3e1b034df7b0556ec6cb94cf7f29b0d92dba9788))
13
+
14
+ ## [2.23.0](https://github.com/RedTurtle/io-sanita-theme/compare/2.22.1...2.23.0) (2026-03-19)
15
+
16
+ ### Features
17
+
18
+ * title in header could be hidden by settings panel and in that case loggo has width auto ([#117](https://github.com/RedTurtle/io-sanita-theme/issues/117)) ([7a7af44](https://github.com/RedTurtle/io-sanita-theme/commit/7a7af4419b61d359db2d5ff26ea1f004e1991d95))
19
+
20
+ ### Bug Fixes
21
+
22
+ * download/file and display-file/file url ([bacf5cf](https://github.com/RedTurtle/io-sanita-theme/commit/bacf5cf1bbc7c90b6ec16d236dea6122fdc9f60c))
23
+ * link color in callout dark ([#118](https://github.com/RedTurtle/io-sanita-theme/issues/118)) ([9bbbf02](https://github.com/RedTurtle/io-sanita-theme/commit/9bbbf0249a158f2dd2b03e05dee15eab0271c0aa))
24
+ * objectbrowser searchableType now consider selectableTypes ([c2976b5](https://github.com/RedTurtle/io-sanita-theme/commit/c2976b5bbc71a41e87f6c13163e5498275109a3d))
25
+
3
26
  ## [2.22.1](https://github.com/RedTurtle/io-sanita-theme/compare/2.21.10...2.22.1) (2025-11-03)
4
27
 
5
28
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "io-sanita-theme",
3
- "version": "2.22.1",
3
+ "version": "2.23.1",
4
4
  "description": "io-sanita-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
@@ -56,6 +56,17 @@
56
56
  }
57
57
 
58
58
  .block.alert_block {
59
+ h1,
60
+ h2,
61
+ h3,
62
+ h4,
63
+ h5,
64
+ h6 {
65
+ margin-top: 0;
66
+ margin-bottom: 0.5rem;
67
+ line-height: 1.2;
68
+ }
69
+
59
70
  .full-width {
60
71
  height: auto;
61
72
  }
@@ -1,10 +1,12 @@
1
1
  import React from 'react';
2
2
  import cx from 'classnames';
3
+ import { useSelector } from 'react-redux';
3
4
  import { useIntl } from 'react-intl';
4
5
  import { SiteProperty } from 'volto-site-settings';
5
6
  import { getSiteProperty } from 'io-sanita-theme/helpers';
6
7
 
7
8
  const BrandText = ({ mobile = false, getParent = false }) => {
9
+ const subsite = useSelector((state) => state.subsite?.data);
8
10
  const intl = useIntl();
9
11
  let title = SiteProperty({
10
12
  property: 'site_title',
@@ -27,8 +29,16 @@ const BrandText = ({ mobile = false, getParent = false }) => {
27
29
  </React.Fragment>
28
30
  ));
29
31
 
32
+ const hide_title = SiteProperty({
33
+ property: 'hide_title',
34
+ defaultValue: false,
35
+ getValue: true,
36
+ getParent: false,
37
+ });
38
+ const visuallyHidden = !subsite && hide_title;
39
+
30
40
  return (
31
- <div className="it-brand-text">
41
+ <div className={cx('it-brand-text', { 'visually-hidden': visuallyHidden })}>
32
42
  {title && <div className="it-brand-title">{title}</div>}
33
43
  {description && (
34
44
  <div
@@ -1,3 +1,11 @@
1
+ import './cardFile.scss';
2
+
3
+ import { Card, CardBody, CardText, CardTitle } from 'design-react-kit';
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+
6
+ import { FileIcon } from 'io-sanita-theme/helpers';
7
+ import { Icon } from 'io-sanita-theme/components';
8
+ import Module from 'io-sanita-theme/components/Cards/CardFile/Module';
1
9
  /*
2
10
  Usa la Card File per
3
11
  - allegare documenti o
@@ -21,17 +29,10 @@ In caso di download diretto, è suggerito l’utilizzo dell’icona con il forma
21
29
  In caso di collegamento a una pagina foglia di tipo documento, è suggerito l’utilizzo di un’icona file generica e non è necessario specificare estensione e peso nel titolo.
22
30
  */
23
31
  import React from 'react';
24
- import { useIntl, defineMessages } from 'react-intl';
32
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
25
33
  import cx from 'classnames';
26
- import { Card, CardBody, CardTitle, CardText } from 'design-react-kit';
27
34
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
28
- import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
29
35
  import { viewDate } from 'io-sanita-theme/helpers';
30
- import { Icon } from 'io-sanita-theme/components';
31
- import { FileIcon } from 'io-sanita-theme/helpers';
32
- import Module from 'io-sanita-theme/components/Cards/CardFile/Module';
33
-
34
- import './cardFile.scss';
35
36
 
36
37
  const messages = defineMessages({
37
38
  attachment: {
@@ -154,12 +155,12 @@ export const CardFile = ({
154
155
  <a
155
156
  className="card-title-link flex-grow-1 pe-4"
156
157
  href={flattenToAppURL(file.download)}
157
- title={file.filename}
158
+ title={decodeURI(file.filename)}
158
159
  target={pdfFile ? '_blank' : '_self'}
159
160
  rel={pdfFile ? 'noopener noreferrer' : ''}
160
161
  data-element={titleDataElement}
161
162
  >
162
- {file.filename}
163
+ {decodeURI(file.filename)}
163
164
  </a>
164
165
  )}
165
166
 
@@ -0,0 +1,65 @@
1
+ import type { ConfigType } from '@plone/registry';
2
+ import { FeedbackForm } from 'volto-feedback-italia';
3
+ import Icon from 'io-sanita-theme/components/Icon/Icon';
4
+ import ImageWithErrors from 'io-sanita-theme/components/ImageWithErrors/ImageWithErrors';
5
+ import Pagination from 'io-sanita-theme/components/Pagination/Pagination';
6
+ import RichTextSection from 'io-sanita-theme/helpers/RichText/RichTextSection';
7
+ import SelectInput from 'io-sanita-theme/components/Widgets/SelectInput/SelectInput';
8
+ import SideMenu from 'io-sanita-theme/components/View/commons/SideMenu/SideMenu';
9
+ import SiteSettingsExtras from 'io-sanita-theme/components/AppExtras/SiteSettingsExtras';
10
+ import SkipToMainContent from 'io-sanita-theme/components/View/commons/SkipToMainContent';
11
+
12
+ export default function applyComponentConfig(config: ConfigType) {
13
+ config.components = {
14
+ ...config.components,
15
+ BlockExtraTags: { component: () => null },
16
+ Image: {
17
+ // @ts-expect-error
18
+ component: ImageWithErrors,
19
+ },
20
+ };
21
+
22
+ config.registerComponent({
23
+ name: 'FeedbackForm',
24
+ component: FeedbackForm,
25
+ });
26
+
27
+ config.registerComponent({
28
+ name: 'Icon',
29
+ component: Icon,
30
+ });
31
+
32
+ config.registerComponent({
33
+ name: 'Pagination',
34
+ component: Pagination,
35
+ });
36
+
37
+ config.registerComponent({
38
+ name: 'RichTextSection',
39
+ // @ts-expect-error
40
+ component: RichTextSection,
41
+ });
42
+
43
+ config.registerComponent({
44
+ name: 'SelectInput',
45
+ component: SelectInput,
46
+ });
47
+
48
+ config.registerComponent({
49
+ name: 'SideMenu',
50
+ // @ts-expect-error
51
+ component: SideMenu,
52
+ });
53
+
54
+ config.registerComponent({
55
+ name: 'SiteSettingsExtras',
56
+ component: SiteSettingsExtras,
57
+ });
58
+
59
+ config.registerComponent({
60
+ name: 'SkipToMainContent',
61
+ component: SkipToMainContent,
62
+ });
63
+
64
+ return config;
65
+ }
@@ -1,6 +1,5 @@
1
1
  import loadable from '@loadable/component';
2
2
  import { defineMessages } from 'react-intl';
3
- import ImageWithErrors from 'io-sanita-theme/components/ImageWithErrors/ImageWithErrors';
4
3
 
5
4
  import menuSVG from '@plone/volto/icons/menu.svg';
6
5
  import menuAltSVG from '@plone/volto/icons/menu-alt.svg';
@@ -43,6 +42,7 @@ import TrackFocus from 'io-sanita-theme/components/AppExtras/TrackFocus';
43
42
  import HandleAnchor from 'io-sanita-theme/components/AppExtras/HandleAnchor';
44
43
  import SiteSettingsExtras from 'io-sanita-theme/components/AppExtras/SiteSettingsExtras';
45
44
  import GenericAppExtras from 'io-sanita-theme/components/AppExtras/GenericAppExtras';
45
+ import applyComponentConfig from 'io-sanita-theme/config/components';
46
46
  import { loadables as IoSanitaLoadables } from 'io-sanita-theme/config/loadables';
47
47
  import { registerIOSanitaValidators } from 'io-sanita-theme/config/validators';
48
48
 
@@ -58,6 +58,7 @@ import applyIoSanitaViews from 'io-sanita-theme/config/views/views';
58
58
  import { SideMenu } from 'io-sanita-theme/components/View/commons';
59
59
  import AggregationPage from 'io-sanita-theme/components/View/AggregationPage/AggregationPage';
60
60
  import { applyFarmacieConfig } from './farmacie';
61
+ import applyUtilitiesConfig from 'io-sanita-theme/config/utilities';
61
62
 
62
63
  import getIoSanitaWidgets from 'io-sanita-theme/config/widgets/widgets';
63
64
  import { component } from 'design-react-kit/dist/types/Icon/assets/ItAndroidSquare';
@@ -321,19 +322,10 @@ export default function applyConfig(config) {
321
322
  removeListingVariation(config, 'summary'); // removes summary volto template, because is unused
322
323
  removeListingVariation(config, 'imageGallery'); // removes imageGallery volto template, because we have our photoGallery template
323
324
 
324
- // COMPONENTS
325
- config.components = {
326
- ...config.components,
327
- BlockExtraTags: { component: () => null },
328
- Image: {
329
- component: ImageWithErrors,
330
- },
331
- };
332
-
333
- config.registerComponent({
334
- name: 'SiteSettingsExtras',
335
- component: SiteSettingsExtras,
336
- });
325
+ // /******************************************************************************
326
+ // * COMPONENTS
327
+ // ******************************************************************************/
328
+ applyComponentConfig(config);
337
329
 
338
330
  // REDUCERS
339
331
  config.addonReducers = {
@@ -341,6 +333,11 @@ export default function applyConfig(config) {
341
333
  ...reducers,
342
334
  };
343
335
 
336
+ // /******************************************************************************
337
+ // * UTILITIES
338
+ // ******************************************************************************/
339
+ applyUtilitiesConfig(config);
340
+
344
341
  // VALIDATORS
345
342
  registerIOSanitaValidators(config);
346
343
 
@@ -0,0 +1,18 @@
1
+ import type { ConfigType } from '@plone/registry';
2
+ import { viewDate } from 'io-sanita-theme/helpers';
3
+
4
+ export default function applyUtilitiesConfig(config: ConfigType) {
5
+ config.registerUtility({
6
+ name: 'viewDate',
7
+ type: 'italia-viewDate',
8
+ method: viewDate,
9
+ });
10
+
11
+ return config;
12
+ }
13
+
14
+ declare module '@plone/types' {
15
+ interface UtilityTypeMap {
16
+ 'italia-viewDate': typeof viewDate;
17
+ }
18
+ }
@@ -0,0 +1,629 @@
1
+ /* CUSTOMIZATIONS:
2
+ - backport of https://github.com/plone/volto/pull/7915
3
+ */
4
+ import React, { Component } from 'react';
5
+ import PropTypes from 'prop-types';
6
+ import { compose } from 'redux';
7
+ import { connect } from 'react-redux';
8
+ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
9
+ import { Input, Segment, Breadcrumb } from 'semantic-ui-react';
10
+
11
+ import join from 'lodash/join';
12
+
13
+ // These absolute imports (without using the corresponding centralized index.js) are required
14
+ // to cut circular import problems, this file should never use them. This is because of
15
+ // the very nature of the functionality of the component and its relationship with others
16
+ import { searchContent } from '@plone/volto/actions/search/search';
17
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
18
+ import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url';
19
+ import config from '@plone/volto/registry';
20
+
21
+ import backSVG from '@plone/volto/icons/back.svg';
22
+ import folderSVG from '@plone/volto/icons/folder.svg';
23
+ import clearSVG from '@plone/volto/icons/clear.svg';
24
+ import searchSVG from '@plone/volto/icons/zoom.svg';
25
+ import linkSVG from '@plone/volto/icons/link.svg';
26
+ import homeSVG from '@plone/volto/icons/home.svg';
27
+ import iconsSVG from '@plone/volto/icons/apps.svg';
28
+ import listSVG from '@plone/volto/icons/list-bullet.svg';
29
+
30
+ import ObjectBrowserNav from '@plone/volto/components/manage/Sidebar/ObjectBrowserNav';
31
+
32
+ const messages = defineMessages({
33
+ SearchInputPlaceholder: {
34
+ id: 'Search content',
35
+ defaultMessage: 'Search content',
36
+ },
37
+ SelectedItems: {
38
+ id: 'Selected items',
39
+ defaultMessage: 'Selected items',
40
+ },
41
+ back: {
42
+ id: 'Back',
43
+ defaultMessage: 'Back',
44
+ },
45
+ search: {
46
+ id: 'Search SVG',
47
+ defaultMessage: 'Search SVG',
48
+ },
49
+ iconView: {
50
+ id: 'Icon View',
51
+ defaultMessage: 'Icon View',
52
+ },
53
+ listView: {
54
+ id: 'List View',
55
+ defaultMessage: 'List View',
56
+ },
57
+ home: {
58
+ id: 'Home',
59
+ defaultMessage: 'Home',
60
+ },
61
+ of: { id: 'Selected items - x of y', defaultMessage: 'of' },
62
+ });
63
+
64
+ function getParentURL(url) {
65
+ return flattenToAppURL(`${join(url.split('/').slice(0, -1), '/')}`) || '/';
66
+ }
67
+
68
+ /**
69
+ * ObjectBrowserBody container class.
70
+ * @class ObjectBrowserBody
71
+ * @extends Component
72
+ */
73
+ class ObjectBrowserBody extends Component {
74
+ /**
75
+ * Property types.
76
+ * @property {Object} propTypes Property types.
77
+ * @static
78
+ */
79
+ static propTypes = {
80
+ block: PropTypes.string.isRequired,
81
+ mode: PropTypes.string.isRequired,
82
+ data: PropTypes.any.isRequired,
83
+ searchSubrequests: PropTypes.objectOf(PropTypes.any).isRequired,
84
+ searchContent: PropTypes.func.isRequired,
85
+ closeObjectBrowser: PropTypes.func.isRequired,
86
+ onChangeBlock: PropTypes.func.isRequired,
87
+ onSelectItem: PropTypes.func,
88
+ dataName: PropTypes.string,
89
+ maximumSelectionSize: PropTypes.number,
90
+ contextURL: PropTypes.string,
91
+ searchableTypes: PropTypes.arrayOf(PropTypes.string),
92
+ };
93
+
94
+ /**
95
+ * Default properties.
96
+ * @property {Object} defaultProps Default properties.
97
+ * @static
98
+ */
99
+ static defaultProps = {
100
+ image: '',
101
+ href: '',
102
+ onSelectItem: null,
103
+ dataName: null,
104
+ selectableTypes: [],
105
+ searchableTypes: null,
106
+ maximumSelectionSize: null,
107
+ };
108
+
109
+ /**
110
+ * Constructor
111
+ * @method constructor
112
+ * @param {Object} props Component properties
113
+ * @constructs WysiwygEditor
114
+ */
115
+ constructor(props) {
116
+ super(props);
117
+ this.state = {
118
+ currentFolder:
119
+ this.props.mode === 'multiple' ? '/' : this.props.contextURL || '/',
120
+ currentImageFolder:
121
+ this.props.mode === 'multiple'
122
+ ? '/'
123
+ : this.props.mode === 'image' && this.props.data?.url
124
+ ? getParentURL(this.props.data.url)
125
+ : '/',
126
+ currentLinkFolder:
127
+ this.props.mode === 'multiple'
128
+ ? '/'
129
+ : this.props.mode === 'link' && this.props.data?.href
130
+ ? getParentURL(this.props.data.href)
131
+ : '/',
132
+ parentFolder: '',
133
+ selectedImage:
134
+ this.props.mode === 'multiple'
135
+ ? ''
136
+ : this.props.mode === 'image' && this.props.data?.url
137
+ ? flattenToAppURL(this.props.data.url)
138
+ : '',
139
+ selectedHref:
140
+ this.props.mode === 'multiple'
141
+ ? ''
142
+ : this.props.mode === 'link' && this.props.data?.href
143
+ ? flattenToAppURL(this.props.data.href)
144
+ : '',
145
+ showSearchInput: false,
146
+ // In image mode, the searchable types default to the image types which
147
+ // can be overridden with the property if specified.
148
+ // If selectableTypes are passed, the searchableTypes are the selectableTypes
149
+ searchableTypes:
150
+ this.props.mode === 'image'
151
+ ? this.props.searchableTypes || config.settings.imageObjects
152
+ : [
153
+ ...(this.props.searchableTypes ?? []),
154
+ ...(this.props.selectableTypes ?? []),
155
+ ],
156
+ view: this.props.mode === 'image' ? 'icons' : 'list',
157
+ };
158
+ this.searchInputRef = React.createRef();
159
+ }
160
+
161
+ /**
162
+ * Component did mount
163
+ * @method componentDidMount
164
+ * @returns {undefined}
165
+ */
166
+ componentDidMount() {
167
+ this.initialSearch(this.props.mode);
168
+ }
169
+
170
+ initialSearch = (mode) => {
171
+ const currentSelected =
172
+ mode === 'multiple'
173
+ ? ''
174
+ : mode === 'image'
175
+ ? this.state.selectedImage
176
+ : this.state.selectedHref;
177
+ if (currentSelected && isInternalURL(currentSelected)) {
178
+ this.props.searchContent(
179
+ getParentURL(currentSelected),
180
+ {
181
+ 'path.depth': 1,
182
+ sort_on: 'getObjPositionInParent',
183
+ metadata_fields: '_all',
184
+ b_size: 1000,
185
+ },
186
+ `${this.props.block}-${mode}`,
187
+ );
188
+ } else {
189
+ this.props.searchContent(
190
+ this.state.currentFolder,
191
+ {
192
+ 'path.depth': 1,
193
+ sort_on: 'getObjPositionInParent',
194
+ metadata_fields: '_all',
195
+ b_size: 1000,
196
+ },
197
+ `${this.props.block}-${mode}`,
198
+ );
199
+ }
200
+ };
201
+
202
+ navigateTo = (id) => {
203
+ this.props.searchContent(
204
+ id,
205
+ {
206
+ 'path.depth': 1,
207
+ sort_on: 'getObjPositionInParent',
208
+ metadata_fields: '_all',
209
+ b_size: 1000,
210
+ },
211
+ `${this.props.block}-${this.props.mode}`,
212
+ );
213
+ const parent = `${join(id.split('/').slice(0, -1), '/')}` || '/';
214
+ this.setState(() => ({
215
+ parentFolder: parent,
216
+ currentFolder: id || '/',
217
+ }));
218
+ };
219
+
220
+ toggleSearchInput = () =>
221
+ this.setState(
222
+ (prevState) => ({
223
+ showSearchInput: !prevState.showSearchInput,
224
+ }),
225
+ () => {
226
+ if (this.searchInputRef?.current) {
227
+ this.searchInputRef.current.focus();
228
+ } else {
229
+ this.props.searchContent(
230
+ this.state.currentFolder,
231
+ {
232
+ 'path.depth': 1,
233
+ sort_on: 'getObjPositionInParent',
234
+ metadata_fields: '_all',
235
+ b_size: 1000,
236
+ },
237
+ `${this.props.block}-${this.props.mode}`,
238
+ );
239
+ }
240
+ },
241
+ );
242
+
243
+ toggleView = () =>
244
+ this.setState((prevState) => ({
245
+ view: prevState.view === 'icons' ? 'list' : 'icons',
246
+ }));
247
+
248
+ onSearch = (e) => {
249
+ const text = flattenToAppURL(e.target.value);
250
+ if (text.startsWith('/')) {
251
+ this.setState({ currentFolder: text });
252
+ this.props.searchContent(
253
+ text,
254
+ {
255
+ 'path.depth': 1,
256
+ sort_on: 'getObjPositionInParent',
257
+ metadata_fields: '_all',
258
+ portal_type: this.state.searchableTypes,
259
+ },
260
+ `${this.props.block}-${this.props.mode}`,
261
+ );
262
+ } else {
263
+ text.length > 2
264
+ ? this.props.searchContent(
265
+ '/',
266
+ {
267
+ SearchableText: `${text}*`,
268
+ metadata_fields: '_all',
269
+ portal_type: this.state.searchableTypes,
270
+ },
271
+ `${this.props.block}-${this.props.mode}`,
272
+ )
273
+ : this.props.searchContent(
274
+ '/',
275
+ {
276
+ 'path.depth': 1,
277
+ sort_on: 'getObjPositionInParent',
278
+ metadata_fields: '_all',
279
+ portal_type: this.state.searchableTypes,
280
+ },
281
+ `${this.props.block}-${this.props.mode}`,
282
+ );
283
+ }
284
+ };
285
+
286
+ onSelectItem = (item) => {
287
+ const url = item['@id'];
288
+ const { block, data, mode, dataName, onChangeBlock } = this.props;
289
+
290
+ const updateState = (mode) => {
291
+ switch (mode) {
292
+ case 'image':
293
+ this.setState({
294
+ selectedImage: url,
295
+ currentImageFolder: getParentURL(url),
296
+ });
297
+ break;
298
+ case 'link':
299
+ this.setState({
300
+ selectedHref: url,
301
+ currentLinkFolder: getParentURL(url),
302
+ });
303
+ break;
304
+ default:
305
+ break;
306
+ }
307
+ };
308
+
309
+ if (dataName) {
310
+ onChangeBlock(block, {
311
+ ...data,
312
+ [dataName]: url,
313
+ });
314
+ } else if (this.props.onSelectItem) {
315
+ this.props.onSelectItem(url, item);
316
+ } else if (mode === 'image') {
317
+ onChangeBlock(block, {
318
+ ...data,
319
+ url: flattenToAppURL(item.getURL),
320
+ alt: '',
321
+ });
322
+ } else if (mode === 'link') {
323
+ onChangeBlock(block, {
324
+ ...data,
325
+ href: flattenToAppURL(url),
326
+ });
327
+ }
328
+ updateState(mode);
329
+ };
330
+
331
+ onChangeBlockData = (key, value) => {
332
+ this.props.onChangeBlock(this.props.block, {
333
+ ...this.props.data,
334
+ [key]: value,
335
+ });
336
+ };
337
+
338
+ isSelectable = (item) => {
339
+ const { maximumSelectionSize, data, mode, selectableTypes } = this.props;
340
+ if (
341
+ maximumSelectionSize &&
342
+ data &&
343
+ mode === 'multiple' &&
344
+ maximumSelectionSize <= data.length
345
+ )
346
+ // The item should actually be selectable, but only for removing it from already selected items list.
347
+ // handleClickOnItem will handle the deselection logic.
348
+ // The item is not selectable if we reached/exceeded maximumSelectionSize and is not already selected.
349
+ return data.some(
350
+ (d) => flattenToAppURL(d['@id']) === flattenToAppURL(item['@id']),
351
+ );
352
+ return selectableTypes.length > 0
353
+ ? selectableTypes.indexOf(item['@type']) >= 0
354
+ : true;
355
+ };
356
+
357
+ handleClickOnItem = (item) => {
358
+ if (this.props.mode === 'image') {
359
+ if (item.is_folderish) {
360
+ this.navigateTo(item['@id']);
361
+ }
362
+ if (config.settings.imageObjects.includes(item['@type'])) {
363
+ this.onSelectItem(item);
364
+ }
365
+ } else {
366
+ if (this.isSelectable(item)) {
367
+ if (
368
+ !this.props.maximumSelectionSize ||
369
+ this.props.mode === 'multiple' ||
370
+ !this.props.data ||
371
+ this.props.data.length <= this.props.maximumSelectionSize
372
+ ) {
373
+ let isDeselecting;
374
+ if (this.props.mode === 'multiple' && Array.isArray(this.props.data))
375
+ isDeselecting = this.props.data.some(
376
+ (d) => flattenToAppURL(d['@id']) === flattenToAppURL(item['@id']),
377
+ );
378
+ this.onSelectItem(item);
379
+ let length = this.props.data ? this.props.data.length : 0;
380
+ let stopSelecting = this.props.mode !== 'multiple';
381
+ if (isDeselecting && !stopSelecting)
382
+ stopSelecting =
383
+ this.props.maximumSelectionSize > 0 &&
384
+ length - 1 >= this.props.maximumSelectionSize;
385
+ else
386
+ stopSelecting =
387
+ this.props.maximumSelectionSize > 0 &&
388
+ length + 1 >= this.props.maximumSelectionSize;
389
+ if (stopSelecting) {
390
+ this.props.closeObjectBrowser();
391
+ }
392
+ } else {
393
+ this.props.closeObjectBrowser();
394
+ }
395
+ } else {
396
+ this.navigateTo(item['@id']);
397
+ }
398
+ }
399
+ };
400
+
401
+ handleDoubleClickOnItem = (item) => {
402
+ if (this.props.mode === 'image') {
403
+ if (item.is_folderish) {
404
+ this.navigateTo(item['@id']);
405
+ }
406
+ if (config.settings.imageObjects.includes(item['@type'])) {
407
+ this.onSelectItem(item);
408
+ this.props.closeObjectBrowser();
409
+ }
410
+ } else {
411
+ if (this.isSelectable(item)) {
412
+ if (this.props.data.length < this.props.maximumSelectionSize) {
413
+ this.onSelectItem(item);
414
+ }
415
+ this.props.closeObjectBrowser();
416
+ } else {
417
+ this.navigateTo(item['@id']);
418
+ }
419
+ }
420
+ };
421
+
422
+ /**
423
+ * Render method.
424
+ * @method render
425
+ * @returns {string} Markup for the component.
426
+ */
427
+ render() {
428
+ return (
429
+ <Segment.Group raised className="object-browser">
430
+ <header className="header pulled">
431
+ <div className="vertical divider" />
432
+ {this.state.showSearchInput ? (
433
+ <Input
434
+ className="search"
435
+ ref={this.searchInputRef}
436
+ onChange={this.onSearch}
437
+ placeholder={this.props.intl.formatMessage(
438
+ messages.SearchInputPlaceholder,
439
+ )}
440
+ />
441
+ ) : (
442
+ <>
443
+ {this.state.currentFolder === '/' ? (
444
+ <>
445
+ {this.props.mode === 'image' ? (
446
+ <Icon name={folderSVG} size="24px" />
447
+ ) : (
448
+ <Icon name={linkSVG} size="24px" />
449
+ )}
450
+ </>
451
+ ) : (
452
+ <button
453
+ aria-label={this.props.intl.formatMessage(messages.back)}
454
+ onClick={() => this.navigateTo(this.state.parentFolder)}
455
+ >
456
+ <Icon name={backSVG} size="24px" />
457
+ </button>
458
+ )}
459
+ {this.props.mode === 'image' ? (
460
+ <h2>
461
+ <FormattedMessage
462
+ id="Choose Image"
463
+ defaultMessage="Choose Image"
464
+ />
465
+ </h2>
466
+ ) : (
467
+ <h2>
468
+ <FormattedMessage
469
+ id="Choose Target"
470
+ defaultMessage="Choose Target"
471
+ />
472
+ </h2>
473
+ )}
474
+ </>
475
+ )}
476
+
477
+ <button
478
+ aria-label={this.props.intl.formatMessage(messages.search)}
479
+ onClick={this.toggleSearchInput}
480
+ >
481
+ <Icon name={searchSVG} size="24px" />
482
+ </button>
483
+ <button className="clearSVG" onClick={this.props.closeObjectBrowser}>
484
+ <Icon name={clearSVG} size="24px" />
485
+ </button>
486
+ </header>
487
+ <Segment secondary className="breadcrumbs" vertical>
488
+ {this.props.mode === 'image' && (
489
+ <button
490
+ onClick={this.toggleView}
491
+ className="mode-switch"
492
+ aria-label={this.props.intl.formatMessage(
493
+ this.state.view === 'list'
494
+ ? messages.iconView
495
+ : messages.listView,
496
+ )}
497
+ >
498
+ <Icon
499
+ name={this.state.view === 'list' ? iconsSVG : listSVG}
500
+ size="24px"
501
+ className="mode-switch"
502
+ title={this.props.intl.formatMessage(
503
+ this.state.view === 'list'
504
+ ? messages.iconView
505
+ : messages.listView,
506
+ )}
507
+ />
508
+ </button>
509
+ )}
510
+ {!this.state.showSearchInput ? (
511
+ <Breadcrumb>
512
+ {this.state.currentFolder !== '/' ? (
513
+ this.state.currentFolder
514
+ .split('/')
515
+ .map((item, index, items) => {
516
+ return (
517
+ <React.Fragment key={`divider-${item}-${index}`}>
518
+ {index === 0 ? (
519
+ <Breadcrumb.Section
520
+ onClick={() => this.navigateTo('/')}
521
+ role="button"
522
+ aria-label={this.props.intl.formatMessage(
523
+ messages.home,
524
+ )}
525
+ >
526
+ <Icon
527
+ className="home-icon"
528
+ name={homeSVG}
529
+ size="18px"
530
+ title={this.props.intl.formatMessage(
531
+ messages.home,
532
+ )}
533
+ />
534
+ </Breadcrumb.Section>
535
+ ) : (
536
+ <>
537
+ <Breadcrumb.Divider key={`divider-${item.url}`} />
538
+ <Breadcrumb.Section
539
+ role="button"
540
+ onClick={() =>
541
+ this.navigateTo(
542
+ items.slice(0, index + 1).join('/'),
543
+ )
544
+ }
545
+ >
546
+ {item}
547
+ </Breadcrumb.Section>
548
+ </>
549
+ )}
550
+ </React.Fragment>
551
+ );
552
+ })
553
+ ) : (
554
+ <Breadcrumb.Section
555
+ onClick={() => this.navigateTo('/')}
556
+ aria-label={this.props.intl.formatMessage(messages.home)}
557
+ >
558
+ <Icon
559
+ className="home-icon"
560
+ name={homeSVG}
561
+ role="button"
562
+ size="18px"
563
+ title={this.props.intl.formatMessage(messages.home)}
564
+ />
565
+ </Breadcrumb.Section>
566
+ )}
567
+ </Breadcrumb>
568
+ ) : (
569
+ <div className="searchResults">
570
+ <FormattedMessage
571
+ id="Search results"
572
+ defaultMessage="Search results"
573
+ />
574
+ </div>
575
+ )}
576
+ </Segment>
577
+ {this.props.mode === 'multiple' && (
578
+ <Segment className="infos">
579
+ {this.props.intl.formatMessage(messages.SelectedItems)}:{' '}
580
+ {this.props.data?.length}
581
+ {this.props.maximumSelectionSize > 0 && (
582
+ <>
583
+ {' '}
584
+ {this.props.intl.formatMessage(messages.of)}{' '}
585
+ {this.props.maximumSelectionSize}
586
+ </>
587
+ )}
588
+ </Segment>
589
+ )}
590
+ <ObjectBrowserNav
591
+ currentSearchResults={
592
+ this.props.searchSubrequests[
593
+ `${this.props.block}-${this.props.mode}`
594
+ ]
595
+ }
596
+ selected={
597
+ this.props.mode === 'multiple'
598
+ ? this.props.data
599
+ : [
600
+ {
601
+ '@id':
602
+ this.props.mode === 'image'
603
+ ? this.state.selectedImage
604
+ : this.state.selectedHref,
605
+ },
606
+ ]
607
+ }
608
+ handleClickOnItem={this.handleClickOnItem}
609
+ handleDoubleClickOnItem={this.handleDoubleClickOnItem}
610
+ mode={this.props.mode}
611
+ view={this.state.view}
612
+ navigateTo={this.navigateTo}
613
+ isSelectable={this.isSelectable}
614
+ />
615
+ </Segment.Group>
616
+ );
617
+ }
618
+ }
619
+
620
+ export default compose(
621
+ injectIntl,
622
+ connect(
623
+ (state) => ({
624
+ searchSubrequests: state.search.subrequests,
625
+ lang: state.intl.locale,
626
+ }),
627
+ { searchContent },
628
+ ),
629
+ )(ObjectBrowserBody);
@@ -5,7 +5,7 @@
5
5
  * CUSTOMIZATIONS:
6
6
  * - aggiunto icona per link esterni
7
7
  * - aggiunto title informativo per link esterni
8
- * - aggiunta condizione per non avere un duplicato di @@download/file p @@display-file/file, perchè su io-sanità arriva dal BE anche per gli anonimi
8
+ * - aggiunta condizione per non avere un duplicato di @@download/file e @@display-file/file, perchè su io-sanità arriva dal BE anche per gli anonimi
9
9
  */
10
10
 
11
11
  import React from 'react';
@@ -64,7 +64,8 @@ const UniversalLink = ({
64
64
  if (
65
65
  !token &&
66
66
  config.settings.downloadableObjects.includes(item['@type']) &&
67
- url.indexOf('@@download') < 0 //aggiunta condizione per non avere un duplicato di @@download/file, perchè su io-sanità arriva dal BE anche per gli anonimi
67
+ url.indexOf('@@download') < 0 && //aggiunta condizione per non avere un duplicato di @@download/file, perchè su io-sanità arriva dal BE anche per gli anonimi
68
+ url.indexOf('@@display-file') < 0 //aggiunta condizione per non avere sia @@download/file che @@display-file/file, perchè su io-sanità arriva dal BE anche per gli anonimi
68
69
  ) {
69
70
  url = `${url}/@@download/file`;
70
71
  }
@@ -72,7 +73,8 @@ const UniversalLink = ({
72
73
  if (
73
74
  !token &&
74
75
  config.settings.viewableInBrowserObjects.includes(item['@type']) &&
75
- url.indexOf('@@display-file') < 0 //aggiunta condizione per non avere un duplicato di @@display-file/file, perchè su io-sanità arriva dal BE anche per gli anonimi
76
+ url.indexOf('@@display-file') < 0 && //aggiunta condizione per non avere un duplicato di @@display-file/file, perchè su io-sanità arriva dal BE anche per gli anonimi
77
+ url.indexOf('@@download') < 0 //aggiunta condizione per non avere sia @@download/file che @@display-file/file, perchè su io-sanità arriva dal BE anche per gli anonimi
76
78
  ) {
77
79
  url = `${url}/@@display-file/file`;
78
80
  }
@@ -32,7 +32,7 @@ const Footer = ({ intl }) => {
32
32
  contentType = null;
33
33
  }
34
34
 
35
- const NoFeedbackFormFor =
35
+ const noFeedbackFormFor =
36
36
  config.settings.siteProperties.noFeedbackFormFor || [];
37
37
  const showFeedbackForm = config.settings.siteProperties
38
38
  ?.enableNoFeedbackFormFor
@@ -2,12 +2,14 @@
2
2
  * File view component.
3
3
  * @module components/theme/View/FileView
4
4
  * - changed card layout
5
+ * - decodeURI filename
5
6
  */
6
7
 
7
- import React from 'react';
8
- import PropTypes from 'prop-types';
9
- import { Container, Row, Col } from 'design-react-kit';
8
+ import { Col, Container, Row } from 'design-react-kit';
9
+
10
10
  import { DownloadFileFormat } from 'io-sanita-theme/helpers';
11
+ import PropTypes from 'prop-types';
12
+ import React from 'react';
11
13
 
12
14
  /**
13
15
  * File view component class.
@@ -35,7 +37,7 @@ const FileView = ({ content }) => (
35
37
  <Col className="card-wrapper card-teaser-wrapper">
36
38
  <div className="genericcard card card-teaser shadow p-4 mt-3 rounded">
37
39
  <div className="card-body">
38
- <h2 className="card-title h5">{content.file.filename}</h2>
40
+ <h2 className="card-title h5">{decodeURI(content.file.filename)}</h2>
39
41
  <DownloadFileFormat file={content.file} iconSize="2x" />
40
42
  </div>
41
43
  </div>
@@ -1,10 +1,8 @@
1
- import React from 'react';
1
+ import { FileIcon } from 'io-sanita-theme/helpers';
2
2
  import PropTypes from 'prop-types';
3
-
3
+ import React from 'react';
4
4
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
5
5
 
6
- import { FileIcon } from 'io-sanita-theme/helpers';
7
-
8
6
  const DownloadFileFormat = ({
9
7
  file,
10
8
  formatsize = '2x',
@@ -16,7 +14,7 @@ const DownloadFileFormat = ({
16
14
  return file ? (
17
15
  <a
18
16
  href={flattenToAppURL(file.download)}
19
- title={file.filename}
17
+ title={decodeURI(file.filename)}
20
18
  className={className}
21
19
  target={pdfFile ? '_blank' : '_self'}
22
20
  rel={pdfFile ? 'noopener noreferrer' : ''}
@@ -322,3 +322,10 @@ body.page-not-found {
322
322
  }
323
323
  }
324
324
  }
325
+
326
+ .blockquote-card.dark a {
327
+ &,
328
+ &:hover {
329
+ color: $primary-text;
330
+ }
331
+ }
@@ -0,0 +1,10 @@
1
+ .public-ui
2
+ .it-header-center-wrapper
3
+ .it-header-center-content-wrapper
4
+ .it-brand-wrapper
5
+ a,
6
+ .navbar .navbar-collapsable .menu-wrapper .it-brand-wrapper a {
7
+ .icon:has(+ .it-brand-text.visually-hidden) {
8
+ width: auto;
9
+ }
10
+ }
@@ -241,6 +241,13 @@
241
241
  &.dark {
242
242
  background-color: $subsite-primary;
243
243
  color: $subsite-primary-text;
244
+
245
+ a {
246
+ &,
247
+ &:hover {
248
+ color: $subsite-primary-text;
249
+ }
250
+ }
244
251
  }
245
252
  }
246
253
  }
@@ -18,6 +18,7 @@
18
18
  @import './io-sanita/addons/volto-data-grid-widget';
19
19
  @import './io-sanita/addons/volto-form-block';
20
20
  @import './io-sanita/components/layout/navbar';
21
+ @import './io-sanita/components/layout/logo';
21
22
  @import './io-sanita/components/layout/mobileMenu';
22
23
  @import './io-sanita/components/layout/tertiaryMenu';
23
24
  @import './io-sanita/components/layout/footer';
package/tsconfig.json CHANGED
@@ -17,7 +17,9 @@
17
17
  "noFallthroughCasesInSwitch": true,
18
18
  "downlevelIteration": true,
19
19
  "incremental": true,
20
- "paths": {}
20
+ "paths": {
21
+ "io-sanita-theme/*": ["./src/*"]
22
+ }
21
23
  },
22
24
  "include": ["src"],
23
25
  "exclude": ["node_modules"]