io-sanita-theme 2.11.0 → 2.11.2
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 +18 -0
- package/RELEASE.md +9 -0
- package/locales/de/LC_MESSAGES/volto.po +10 -0
- package/locales/en/LC_MESSAGES/volto.po +10 -0
- package/locales/es/LC_MESSAGES/volto.po +10 -0
- package/locales/fr/LC_MESSAGES/volto.po +10 -0
- package/locales/it/LC_MESSAGES/volto.po +10 -0
- package/locales/volto.pot +11 -1
- package/package.json +1 -1
- package/src/components/Blocks/Listing/Table/TableTemplate.jsx +14 -4
- package/src/components/View/Widgets/SelectViewWidget.jsx +18 -0
- package/src/components/View/commons/Attachments/Attachments.jsx +1 -1
- package/src/config/ioSanitaConfig.js +3 -3
- package/src/config/widgets/widgets.js +11 -2
- package/src/customizations/@plone/volto-slate/blocks/Table/TableBlockView.jsx +182 -0
- package/src/customizations/volto/components/manage/Widgets/ImageWidget.jsx +343 -0
- package/src/overrideTranslations.jsx +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.11.2](https://github.com/RedTurtle/io-sanita-theme/compare/2.11.1...2.11.2) (2025-04-08)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* fixed title tag for a11y in Attachments component not displayed as section ([#79](https://github.com/RedTurtle/io-sanita-theme/issues/79)) ([96bed38](https://github.com/RedTurtle/io-sanita-theme/commit/96bed38f4441dde74e3e059d7c3a05ad138a7f24))
|
|
8
|
+
* table block sorting ([#78](https://github.com/RedTurtle/io-sanita-theme/issues/78)) ([2e1dfca](https://github.com/RedTurtle/io-sanita-theme/commit/2e1dfcad4faaafddc4991b96deb58078b56fd9be))
|
|
9
|
+
|
|
10
|
+
## [2.11.1](https://github.com/RedTurtle/io-sanita-theme/compare/2.11.0...2.11.1) (2025-03-31)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* a11y Table block ([#76](https://github.com/RedTurtle/io-sanita-theme/issues/76)) ([773d4d9](https://github.com/RedTurtle/io-sanita-theme/commit/773d4d9da80563ef531bcff4d546dd79cbedce00))
|
|
15
|
+
* display none to -newsletterSubscribe on main footer settings ([f2d61ea](https://github.com/RedTurtle/io-sanita-theme/commit/f2d61eabd36d66ea8e5bb146762c5d9bbec595e5))
|
|
16
|
+
* ImageWidget submit form . Backport of voltopr ([2c6c873](https://github.com/RedTurtle/io-sanita-theme/commit/2c6c873042661006f0ba3d60159630d68b07ed20))
|
|
17
|
+
* locales ([b21cbbf](https://github.com/RedTurtle/io-sanita-theme/commit/b21cbbfa51927914081f912e22a801cc9400073f))
|
|
18
|
+
* remove newsletter subscribe option in footer configuration form ([#77](https://github.com/RedTurtle/io-sanita-theme/issues/77)) ([f6b5bf4](https://github.com/RedTurtle/io-sanita-theme/commit/f6b5bf4c7f7b862c2b7ed06908ee6ac2c0be0413))
|
|
19
|
+
* table template view taxonomy ([1175082](https://github.com/RedTurtle/io-sanita-theme/commit/11750828a8cbf7b414fbd8358204af9aa23e24d6))
|
|
20
|
+
|
|
3
21
|
## [2.11.0](https://github.com/RedTurtle/io-sanita-theme/compare/2.10.0...2.11.0) (2025-03-25)
|
|
4
22
|
|
|
5
23
|
### Features
|
package/RELEASE.md
CHANGED
|
@@ -41,6 +41,15 @@
|
|
|
41
41
|
- ...
|
|
42
42
|
-->
|
|
43
43
|
|
|
44
|
+
## Versione 2.11.1 (31/03/2025)
|
|
45
|
+
|
|
46
|
+
### Fix
|
|
47
|
+
|
|
48
|
+
- sistemato un problema di perdita di dati durante il caricamento di una nuova immagine nel form di inseriemento dati di un ct con i campi a blocchi.
|
|
49
|
+
- sistemata la visualizzazione delle tassonomie nel template 'Tabella' del blocco elenco
|
|
50
|
+
- sistemata l'accessibilità per il blocco Tabella
|
|
51
|
+
- rimosso il flag di configurazione 'Mostra la form di iscrizione' nel footer.
|
|
52
|
+
|
|
44
53
|
## Versione 2.11.0 (25/03/2025)
|
|
45
54
|
|
|
46
55
|
### Novità
|
|
@@ -1077,6 +1077,11 @@ msgstr ""
|
|
|
1077
1077
|
msgid "appStoreLink"
|
|
1078
1078
|
msgstr ""
|
|
1079
1079
|
|
|
1080
|
+
#. Default: "ascending"
|
|
1081
|
+
#: overrideTranslations
|
|
1082
|
+
msgid "ascendingTableSort"
|
|
1083
|
+
msgstr ""
|
|
1084
|
+
|
|
1080
1085
|
#. Default: "Allegato"
|
|
1081
1086
|
#: components/Cards/CardFile/CardFile
|
|
1082
1087
|
msgid "attachment"
|
|
@@ -1727,6 +1732,11 @@ msgstr ""
|
|
|
1727
1732
|
msgid "delete"
|
|
1728
1733
|
msgstr ""
|
|
1729
1734
|
|
|
1735
|
+
#. Default: "descending"
|
|
1736
|
+
#: overrideTranslations
|
|
1737
|
+
msgid "descendingTableSort"
|
|
1738
|
+
msgstr ""
|
|
1739
|
+
|
|
1730
1740
|
#. Default: "Testo per il link al dettaglio"
|
|
1731
1741
|
#: config/blocks/listing/ListingOptions/utils
|
|
1732
1742
|
msgid "detail_link_label"
|
|
@@ -1072,6 +1072,11 @@ msgstr "Opening date"
|
|
|
1072
1072
|
msgid "appStoreLink"
|
|
1073
1073
|
msgstr ""
|
|
1074
1074
|
|
|
1075
|
+
#. Default: "ascending"
|
|
1076
|
+
#: overrideTranslations
|
|
1077
|
+
msgid "ascendingTableSort"
|
|
1078
|
+
msgstr ""
|
|
1079
|
+
|
|
1075
1080
|
#. Default: "Allegato"
|
|
1076
1081
|
#: components/Cards/CardFile/CardFile
|
|
1077
1082
|
msgid "attachment"
|
|
@@ -1722,6 +1727,11 @@ msgstr "Start date"
|
|
|
1722
1727
|
msgid "delete"
|
|
1723
1728
|
msgstr ""
|
|
1724
1729
|
|
|
1730
|
+
#. Default: "descending"
|
|
1731
|
+
#: overrideTranslations
|
|
1732
|
+
msgid "descendingTableSort"
|
|
1733
|
+
msgstr ""
|
|
1734
|
+
|
|
1725
1735
|
#. Default: "Testo per il link al dettaglio"
|
|
1726
1736
|
#: config/blocks/listing/ListingOptions/utils
|
|
1727
1737
|
msgid "detail_link_label"
|
|
@@ -1079,6 +1079,11 @@ msgstr ""
|
|
|
1079
1079
|
msgid "appStoreLink"
|
|
1080
1080
|
msgstr ""
|
|
1081
1081
|
|
|
1082
|
+
#. Default: "ascending"
|
|
1083
|
+
#: overrideTranslations
|
|
1084
|
+
msgid "ascendingTableSort"
|
|
1085
|
+
msgstr ""
|
|
1086
|
+
|
|
1082
1087
|
#. Default: "Allegato"
|
|
1083
1088
|
#: components/Cards/CardFile/CardFile
|
|
1084
1089
|
msgid "attachment"
|
|
@@ -1729,6 +1734,11 @@ msgstr ""
|
|
|
1729
1734
|
msgid "delete"
|
|
1730
1735
|
msgstr ""
|
|
1731
1736
|
|
|
1737
|
+
#. Default: "descending"
|
|
1738
|
+
#: overrideTranslations
|
|
1739
|
+
msgid "descendingTableSort"
|
|
1740
|
+
msgstr ""
|
|
1741
|
+
|
|
1732
1742
|
#. Default: "Testo per il link al dettaglio"
|
|
1733
1743
|
#: config/blocks/listing/ListingOptions/utils
|
|
1734
1744
|
msgid "detail_link_label"
|
|
@@ -1079,6 +1079,11 @@ msgstr ""
|
|
|
1079
1079
|
msgid "appStoreLink"
|
|
1080
1080
|
msgstr ""
|
|
1081
1081
|
|
|
1082
|
+
#. Default: "ascending"
|
|
1083
|
+
#: overrideTranslations
|
|
1084
|
+
msgid "ascendingTableSort"
|
|
1085
|
+
msgstr ""
|
|
1086
|
+
|
|
1082
1087
|
#. Default: "Allegato"
|
|
1083
1088
|
#: components/Cards/CardFile/CardFile
|
|
1084
1089
|
msgid "attachment"
|
|
@@ -1729,6 +1734,11 @@ msgstr ""
|
|
|
1729
1734
|
msgid "delete"
|
|
1730
1735
|
msgstr ""
|
|
1731
1736
|
|
|
1737
|
+
#. Default: "descending"
|
|
1738
|
+
#: overrideTranslations
|
|
1739
|
+
msgid "descendingTableSort"
|
|
1740
|
+
msgstr ""
|
|
1741
|
+
|
|
1732
1742
|
#. Default: "Testo per il link al dettaglio"
|
|
1733
1743
|
#: config/blocks/listing/ListingOptions/utils
|
|
1734
1744
|
msgid "detail_link_label"
|
|
@@ -1072,6 +1072,11 @@ msgstr ""
|
|
|
1072
1072
|
msgid "appStoreLink"
|
|
1073
1073
|
msgstr ""
|
|
1074
1074
|
|
|
1075
|
+
#. Default: "ascending"
|
|
1076
|
+
#: overrideTranslations
|
|
1077
|
+
msgid "ascendingTableSort"
|
|
1078
|
+
msgstr "ordine crescente"
|
|
1079
|
+
|
|
1075
1080
|
#. Default: "Allegato"
|
|
1076
1081
|
#: components/Cards/CardFile/CardFile
|
|
1077
1082
|
msgid "attachment"
|
|
@@ -1722,6 +1727,11 @@ msgstr ""
|
|
|
1722
1727
|
msgid "delete"
|
|
1723
1728
|
msgstr "elimina"
|
|
1724
1729
|
|
|
1730
|
+
#. Default: "descending"
|
|
1731
|
+
#: overrideTranslations
|
|
1732
|
+
msgid "descendingTableSort"
|
|
1733
|
+
msgstr "ordine discendente"
|
|
1734
|
+
|
|
1725
1735
|
#. Default: "Testo per il link al dettaglio"
|
|
1726
1736
|
#: config/blocks/listing/ListingOptions/utils
|
|
1727
1737
|
msgid "detail_link_label"
|
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-03-
|
|
4
|
+
"POT-Creation-Date: 2025-03-31T11:23:16.064Z\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"
|
|
@@ -1074,6 +1074,11 @@ msgstr ""
|
|
|
1074
1074
|
msgid "appStoreLink"
|
|
1075
1075
|
msgstr ""
|
|
1076
1076
|
|
|
1077
|
+
#. Default: "ascending"
|
|
1078
|
+
#: overrideTranslations
|
|
1079
|
+
msgid "ascendingTableSort"
|
|
1080
|
+
msgstr ""
|
|
1081
|
+
|
|
1077
1082
|
#. Default: "Allegato"
|
|
1078
1083
|
#: components/Cards/CardFile/CardFile
|
|
1079
1084
|
msgid "attachment"
|
|
@@ -1724,6 +1729,11 @@ msgstr ""
|
|
|
1724
1729
|
msgid "delete"
|
|
1725
1730
|
msgstr ""
|
|
1726
1731
|
|
|
1732
|
+
#. Default: "descending"
|
|
1733
|
+
#: overrideTranslations
|
|
1734
|
+
msgid "descendingTableSort"
|
|
1735
|
+
msgstr ""
|
|
1736
|
+
|
|
1727
1737
|
#. Default: "Testo per il link al dettaglio"
|
|
1728
1738
|
#: config/blocks/listing/ListingOptions/utils
|
|
1729
1739
|
msgid "detail_link_label"
|
package/package.json
CHANGED
|
@@ -42,9 +42,16 @@ const TableTemplate = (props) => {
|
|
|
42
42
|
const ct_schemas = useSelector((state) => state.ct_schema?.subrequests);
|
|
43
43
|
|
|
44
44
|
useEffect(() => {
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
45
|
+
const cts = columns.reduce((acc, c) => {
|
|
46
|
+
if (acc.indexOf(c.ct) < 0) {
|
|
47
|
+
acc.push(c.ct);
|
|
48
|
+
}
|
|
49
|
+
return acc;
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
cts.forEach((c) => {
|
|
53
|
+
if (!ct_schemas[c]) {
|
|
54
|
+
dispatch(getCTSchema(c));
|
|
48
55
|
}
|
|
49
56
|
});
|
|
50
57
|
}, [columns]);
|
|
@@ -100,7 +107,9 @@ const TableTemplate = (props) => {
|
|
|
100
107
|
|
|
101
108
|
let Widget = views?.getWidget(field);
|
|
102
109
|
|
|
103
|
-
let widget_props = {
|
|
110
|
+
let widget_props = {
|
|
111
|
+
behavior: field_properties.behavior,
|
|
112
|
+
};
|
|
104
113
|
switch (c.field) {
|
|
105
114
|
case 'apertura_bando':
|
|
106
115
|
case 'chiusura_procedimento_bando':
|
|
@@ -116,6 +125,7 @@ const TableTemplate = (props) => {
|
|
|
116
125
|
widget_props.vocabulary =
|
|
117
126
|
field_properties.vocabulary['@id'];
|
|
118
127
|
}
|
|
128
|
+
|
|
119
129
|
render_value = (
|
|
120
130
|
<Widget value={item[c.field]} {...widget_props} />
|
|
121
131
|
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import { render } from 'react-dom';
|
|
4
|
+
|
|
5
|
+
const SelectWidget = ({ value, children, className, behavior }) => {
|
|
6
|
+
let render_value = children
|
|
7
|
+
? children(value?.title || value?.token || value)
|
|
8
|
+
: value?.title || value?.token || value;
|
|
9
|
+
if (behavior?.startsWith('collective.taxonomy')) {
|
|
10
|
+
render_value = render_value.split('»').reverse()[0];
|
|
11
|
+
}
|
|
12
|
+
return value ? (
|
|
13
|
+
<span className={cx(className, 'select', 'widget')}>{render_value}</span>
|
|
14
|
+
) : (
|
|
15
|
+
''
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
export default SelectWidget;
|
|
@@ -98,7 +98,7 @@ const Attachments = ({
|
|
|
98
98
|
</RichTextSection>
|
|
99
99
|
) : (
|
|
100
100
|
<div className="mb-5 mt-3">
|
|
101
|
-
{title && <
|
|
101
|
+
{title && <h3 className="h5">{title}</h3>}
|
|
102
102
|
{attachments.length > 0 && attachments_view}
|
|
103
103
|
{searchResults?.[key]?.loading && !searchResults?.[key]?.loaded && <></>}
|
|
104
104
|
</div>
|
|
@@ -54,8 +54,8 @@ import { component } from 'design-react-kit/dist/types/Icon/assets/ItAndroidSqua
|
|
|
54
54
|
export const AGGREGATION_PAGE_ARGOMENTO = '/argomento/';
|
|
55
55
|
export const AGGREGATION_PAGE_TIPOLOGIA_UTENTE = '/tipologia-utente/';
|
|
56
56
|
|
|
57
|
-
const ReleaseLog = loadable(
|
|
58
|
-
import('io-sanita-theme/components/ReleaseLog/ReleaseLog'),
|
|
57
|
+
const ReleaseLog = loadable(
|
|
58
|
+
() => import('io-sanita-theme/components/ReleaseLog/ReleaseLog'),
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
const messages = defineMessages({
|
|
@@ -223,7 +223,7 @@ export default function applyConfig(config) {
|
|
|
223
223
|
|
|
224
224
|
'volto-editablefooter': {
|
|
225
225
|
...config.settings['volto-editablefooter'],
|
|
226
|
-
options: { socials: true, newsletterSubscribe:
|
|
226
|
+
options: { socials: true, newsletterSubscribe: false },
|
|
227
227
|
},
|
|
228
228
|
|
|
229
229
|
'volto-form-block-italia': {
|
|
@@ -49,7 +49,11 @@ const BlocksViewWidget = loadable(() =>
|
|
|
49
49
|
/* webpackChunkName: "ISWidgetView" */ 'io-sanita-theme/components/View/Widgets/BlocksViewWidget'
|
|
50
50
|
),
|
|
51
51
|
);
|
|
52
|
-
|
|
52
|
+
const SelectViewWidget = loadable(() =>
|
|
53
|
+
import(
|
|
54
|
+
/* webpackChunkName: "ISWidgetView" */ 'io-sanita-theme/components/View/Widgets/SelectViewWidget'
|
|
55
|
+
),
|
|
56
|
+
);
|
|
53
57
|
const PanelsWidget = loadable(() =>
|
|
54
58
|
import(
|
|
55
59
|
/* webpackChunkName: "ISManage" */ 'io-sanita-theme/components/manage/Widgets/PanelsWidget/PanelsWidget'
|
|
@@ -99,7 +103,12 @@ const getIoSanitaWidgets = (config) => {
|
|
|
99
103
|
views: {
|
|
100
104
|
...config.widgets.views,
|
|
101
105
|
id: { ...config.widgets.views.id, parliamo_di: ParliamoDiWidgetView },
|
|
102
|
-
widget: {
|
|
106
|
+
widget: {
|
|
107
|
+
...config.widgets.views.widget,
|
|
108
|
+
blocks: BlocksViewWidget,
|
|
109
|
+
choices: SelectViewWidget,
|
|
110
|
+
},
|
|
111
|
+
choices: SelectViewWidget,
|
|
103
112
|
},
|
|
104
113
|
type: {
|
|
105
114
|
...config.widgets.type,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slate Table block's View component.
|
|
3
|
+
* @module volto-slate/blocks/Table/View
|
|
4
|
+
* Customizations:
|
|
5
|
+
* - aggiunto aria-sort per indicare la direzione dell’ordinamento (ascending o descending). Se la colonna non è ordinata, usa aria-sort="none".
|
|
6
|
+
* - role="columnheader": Aggiunge il ruolo di intestazione di colonna.
|
|
7
|
+
* - Accessibilità tastiera (onKeyDown) per permette l’ordinamento con Enter o Spazio.
|
|
8
|
+
* - tabIndex={0} per rende la cella interattiva tramite tastiera.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useMemo } from 'react';
|
|
12
|
+
import PropTypes from 'prop-types';
|
|
13
|
+
import { Table } from 'semantic-ui-react';
|
|
14
|
+
import {
|
|
15
|
+
serializeNodes,
|
|
16
|
+
serializeNodesToText,
|
|
17
|
+
} from '@plone/volto-slate/editor/render';
|
|
18
|
+
import { Node } from 'slate';
|
|
19
|
+
import { useIntl, defineMessages } from 'react-intl';
|
|
20
|
+
|
|
21
|
+
const messages = defineMessages({
|
|
22
|
+
ascendingTableSort: {
|
|
23
|
+
id: 'ascendingTableSort',
|
|
24
|
+
defaultMessage: 'ascending',
|
|
25
|
+
},
|
|
26
|
+
descendingTableSort: {
|
|
27
|
+
id: 'descendingTableSort',
|
|
28
|
+
defaultMessage: 'descending',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Slate Table block's View class.
|
|
34
|
+
* @class View
|
|
35
|
+
* @param {object} data The table data to render as a table.
|
|
36
|
+
*/
|
|
37
|
+
const View = ({ data }) => {
|
|
38
|
+
const [state, setState] = useState({
|
|
39
|
+
column: null,
|
|
40
|
+
direction: null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const intl = useIntl();
|
|
44
|
+
|
|
45
|
+
const { table } = data;
|
|
46
|
+
const {
|
|
47
|
+
rows,
|
|
48
|
+
fixed,
|
|
49
|
+
compact,
|
|
50
|
+
basic,
|
|
51
|
+
celled,
|
|
52
|
+
inverted,
|
|
53
|
+
striped,
|
|
54
|
+
sortable,
|
|
55
|
+
hideHeaders,
|
|
56
|
+
} = table;
|
|
57
|
+
|
|
58
|
+
const headers = useMemo(() => rows?.[0]?.cells || [], [rows]);
|
|
59
|
+
const rowsData = useMemo(() => {
|
|
60
|
+
return (
|
|
61
|
+
rows?.slice(1).map((row) =>
|
|
62
|
+
row.cells.map((cell) => ({
|
|
63
|
+
...cell,
|
|
64
|
+
value:
|
|
65
|
+
cell.value && Node.string({ children: cell.value }).length > 0
|
|
66
|
+
? serializeNodes(cell.value)
|
|
67
|
+
: '\u00A0',
|
|
68
|
+
valueText:
|
|
69
|
+
cell.value && Node.string({ children: cell.value }).length > 0
|
|
70
|
+
? serializeNodesToText(cell.value)
|
|
71
|
+
: '\u00A0',
|
|
72
|
+
})),
|
|
73
|
+
) || []
|
|
74
|
+
);
|
|
75
|
+
}, [rows]);
|
|
76
|
+
|
|
77
|
+
const sortedRows = useMemo(() => {
|
|
78
|
+
if (state.column === null) return rowsData;
|
|
79
|
+
return [...rowsData].sort((a, b) => {
|
|
80
|
+
const aText = a[state.column].valueText;
|
|
81
|
+
const bText = b[state.column].valueText;
|
|
82
|
+
const isAscending = state.direction === 'ascending';
|
|
83
|
+
if (isAscending ? aText < bText : aText > bText) return -1;
|
|
84
|
+
if (isAscending ? aText > bText : aText < bText) return 1;
|
|
85
|
+
return 0;
|
|
86
|
+
});
|
|
87
|
+
}, [state, rowsData]);
|
|
88
|
+
|
|
89
|
+
const handleSort = (index) => {
|
|
90
|
+
if (!sortable) return;
|
|
91
|
+
setState({
|
|
92
|
+
column: index,
|
|
93
|
+
direction:
|
|
94
|
+
state.column !== index
|
|
95
|
+
? 'ascending'
|
|
96
|
+
: state.direction === 'ascending'
|
|
97
|
+
? 'descending'
|
|
98
|
+
: 'ascending',
|
|
99
|
+
sortLabel:
|
|
100
|
+
state.column !== index
|
|
101
|
+
? intl.formatMessage(messages.ascendingTableSort)
|
|
102
|
+
: state.direction === 'ascending'
|
|
103
|
+
? intl.formatMessage(messages.descendingTableSort)
|
|
104
|
+
: intl.formatMessage(messages.ascendingTableSort),
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{table && (
|
|
111
|
+
<Table
|
|
112
|
+
fixed={fixed}
|
|
113
|
+
compact={compact}
|
|
114
|
+
basic={basic ? 'very' : false}
|
|
115
|
+
celled={celled}
|
|
116
|
+
inverted={inverted}
|
|
117
|
+
striped={striped}
|
|
118
|
+
sortable={sortable}
|
|
119
|
+
className="slate-table-block"
|
|
120
|
+
>
|
|
121
|
+
{!hideHeaders && (
|
|
122
|
+
<Table.Header>
|
|
123
|
+
<Table.Row>
|
|
124
|
+
{headers.map((cell, index) => (
|
|
125
|
+
<Table.HeaderCell
|
|
126
|
+
key={index}
|
|
127
|
+
textAlign="left"
|
|
128
|
+
verticalAlign="middle"
|
|
129
|
+
sorted={state.column === index ? state.direction : null}
|
|
130
|
+
aria-sort={
|
|
131
|
+
state.column === index ? state.sortLabel : 'none'
|
|
132
|
+
}
|
|
133
|
+
role="columnheader"
|
|
134
|
+
onClick={() => handleSort(index)}
|
|
135
|
+
onKeyDown={(e) => {
|
|
136
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
handleSort(index);
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
tabIndex={0}
|
|
142
|
+
>
|
|
143
|
+
{cell.value &&
|
|
144
|
+
Node.string({ children: cell.value }).length > 0
|
|
145
|
+
? serializeNodes(cell.value)
|
|
146
|
+
: '\u00A0'}
|
|
147
|
+
</Table.HeaderCell>
|
|
148
|
+
))}
|
|
149
|
+
</Table.Row>
|
|
150
|
+
</Table.Header>
|
|
151
|
+
)}
|
|
152
|
+
<Table.Body>
|
|
153
|
+
{sortedRows.map((row, rowIndex) => (
|
|
154
|
+
<Table.Row key={rowIndex}>
|
|
155
|
+
{row.map((cell, cellIndex) => (
|
|
156
|
+
<Table.Cell
|
|
157
|
+
key={cellIndex}
|
|
158
|
+
textAlign="left"
|
|
159
|
+
verticalAlign="middle"
|
|
160
|
+
>
|
|
161
|
+
{cell.value}
|
|
162
|
+
</Table.Cell>
|
|
163
|
+
))}
|
|
164
|
+
</Table.Row>
|
|
165
|
+
))}
|
|
166
|
+
</Table.Body>
|
|
167
|
+
</Table>
|
|
168
|
+
)}
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Property types.
|
|
175
|
+
* @property {Object} propTypes Property types.
|
|
176
|
+
* @static
|
|
177
|
+
*/
|
|
178
|
+
View.propTypes = {
|
|
179
|
+
data: PropTypes.objectOf(PropTypes.any).isRequired,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default View;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/*
|
|
2
|
+
customizations: backport of: https://github.com/plone/volto/pull/6879
|
|
3
|
+
|
|
4
|
+
*/
|
|
5
|
+
import React, { useEffect, useRef } from 'react';
|
|
6
|
+
import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
|
|
7
|
+
import { useIntl, defineMessages } from 'react-intl';
|
|
8
|
+
import { useDispatch } from 'react-redux';
|
|
9
|
+
import { useLocation } from 'react-router-dom';
|
|
10
|
+
import loadable from '@loadable/component';
|
|
11
|
+
import { connect } from 'react-redux';
|
|
12
|
+
import { compose } from 'redux';
|
|
13
|
+
import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
|
|
14
|
+
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
flattenToAppURL,
|
|
18
|
+
getBaseUrl,
|
|
19
|
+
isInternalURL,
|
|
20
|
+
} from '@plone/volto/helpers/Url/Url';
|
|
21
|
+
import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
|
|
22
|
+
import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
|
|
23
|
+
import { createContent } from '@plone/volto/actions/content/content';
|
|
24
|
+
import { readAsDataURL } from 'promise-file-reader';
|
|
25
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
26
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
27
|
+
|
|
28
|
+
import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
|
|
29
|
+
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
30
|
+
import navTreeSVG from '@plone/volto/icons/nav.svg';
|
|
31
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
|
32
|
+
import uploadSVG from '@plone/volto/icons/upload.svg';
|
|
33
|
+
|
|
34
|
+
const Dropzone = loadable(() => import('react-dropzone'));
|
|
35
|
+
|
|
36
|
+
export const ImageToolbar = ({ className, data, id, onChange, selected }) => (
|
|
37
|
+
<div className="image-upload-widget-toolbar">
|
|
38
|
+
<Button.Group>
|
|
39
|
+
<Button icon basic onClick={() => onChange(id, null)}>
|
|
40
|
+
<Icon className="circled" name={clearSVG} size="24px" color="#e40166" />
|
|
41
|
+
</Button>
|
|
42
|
+
</Button.Group>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const messages = defineMessages({
|
|
47
|
+
addImage: {
|
|
48
|
+
id: 'Browse the site, drop an image, or type a URL',
|
|
49
|
+
defaultMessage: 'Browse the site, drop an image, or use a URL',
|
|
50
|
+
},
|
|
51
|
+
pickAnImage: {
|
|
52
|
+
id: 'pickAnImage',
|
|
53
|
+
defaultMessage: 'Pick an existing image',
|
|
54
|
+
},
|
|
55
|
+
uploadAnImage: {
|
|
56
|
+
id: 'uploadAnImage',
|
|
57
|
+
defaultMessage: 'Upload an image from your computer',
|
|
58
|
+
},
|
|
59
|
+
linkAnImage: {
|
|
60
|
+
id: 'linkAnImage',
|
|
61
|
+
defaultMessage: 'Enter a URL to an image',
|
|
62
|
+
},
|
|
63
|
+
uploadingImage: {
|
|
64
|
+
id: 'Uploading image',
|
|
65
|
+
defaultMessage: 'Uploading image',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const UnconnectedImageInput = (props) => {
|
|
70
|
+
const {
|
|
71
|
+
id,
|
|
72
|
+
onChange,
|
|
73
|
+
onFocus,
|
|
74
|
+
openObjectBrowser,
|
|
75
|
+
value,
|
|
76
|
+
imageSize = 'teaser',
|
|
77
|
+
selected = true,
|
|
78
|
+
hideLinkPicker = false,
|
|
79
|
+
hideObjectBrowserPicker = false,
|
|
80
|
+
restrictFileUpload = false,
|
|
81
|
+
objectBrowserPickerType = 'image',
|
|
82
|
+
description,
|
|
83
|
+
placeholderLinkInput = '',
|
|
84
|
+
onSelectItem,
|
|
85
|
+
} = props;
|
|
86
|
+
const imageValue = value?.[0]?.['@id'] || value;
|
|
87
|
+
|
|
88
|
+
const intl = useIntl();
|
|
89
|
+
const linkEditor = useLinkEditor();
|
|
90
|
+
const location = useLocation();
|
|
91
|
+
const dispatch = useDispatch();
|
|
92
|
+
const contextUrl = location.pathname;
|
|
93
|
+
|
|
94
|
+
const [uploading, setUploading] = React.useState(false);
|
|
95
|
+
const [dragging, setDragging] = React.useState(false);
|
|
96
|
+
|
|
97
|
+
const imageUploadInputRef = useRef(null);
|
|
98
|
+
|
|
99
|
+
const requestId = `image-upload-${id}`;
|
|
100
|
+
|
|
101
|
+
const loaded = props.request.loaded;
|
|
102
|
+
const { content } = props;
|
|
103
|
+
const imageId = content?.['@id'];
|
|
104
|
+
const image = content?.image;
|
|
105
|
+
let loading = false;
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (uploading && loading && loaded) {
|
|
109
|
+
setUploading(false);
|
|
110
|
+
onChange(id, imageId, {
|
|
111
|
+
image_field: 'image',
|
|
112
|
+
image_scales: { image: [image] },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
|
|
116
|
+
|
|
117
|
+
loading = usePrevious(props.request?.loading);
|
|
118
|
+
|
|
119
|
+
const handleUpload = React.useCallback(
|
|
120
|
+
(eventOrFile) => {
|
|
121
|
+
if (restrictFileUpload === true) return;
|
|
122
|
+
eventOrFile.target && eventOrFile.stopPropagation();
|
|
123
|
+
|
|
124
|
+
setUploading(true);
|
|
125
|
+
const file = eventOrFile.target
|
|
126
|
+
? eventOrFile.target.files[0]
|
|
127
|
+
: eventOrFile[0];
|
|
128
|
+
if (!validateFileUploadSize(file, intl.formatMessage)) return;
|
|
129
|
+
readAsDataURL(file).then((fileData) => {
|
|
130
|
+
const fields = fileData.match(/^data:(.*);(.*),(.*)$/);
|
|
131
|
+
dispatch(
|
|
132
|
+
createContent(
|
|
133
|
+
getBaseUrl(contextUrl),
|
|
134
|
+
{
|
|
135
|
+
'@type': 'Image',
|
|
136
|
+
title: file.name,
|
|
137
|
+
image: {
|
|
138
|
+
data: fields[3],
|
|
139
|
+
encoding: fields[2],
|
|
140
|
+
'content-type': fields[1],
|
|
141
|
+
filename: file.name,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
props.block || requestId,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
[
|
|
150
|
+
restrictFileUpload,
|
|
151
|
+
intl.formatMessage,
|
|
152
|
+
dispatch,
|
|
153
|
+
props,
|
|
154
|
+
contextUrl,
|
|
155
|
+
requestId,
|
|
156
|
+
],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const onDragEnter = React.useCallback(() => {
|
|
160
|
+
if (restrictFileUpload === false) setDragging(true);
|
|
161
|
+
}, [restrictFileUpload]);
|
|
162
|
+
const onDragLeave = React.useCallback(() => setDragging(false), []);
|
|
163
|
+
|
|
164
|
+
return imageValue ? (
|
|
165
|
+
<div
|
|
166
|
+
className="image-upload-widget-image"
|
|
167
|
+
onClick={onFocus}
|
|
168
|
+
onKeyDown={onFocus}
|
|
169
|
+
role="toolbar"
|
|
170
|
+
>
|
|
171
|
+
{selected && <ImageToolbar {...props} />}
|
|
172
|
+
<img
|
|
173
|
+
className={props.className}
|
|
174
|
+
src={
|
|
175
|
+
isInternalURL(imageValue)
|
|
176
|
+
? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
|
|
177
|
+
: imageValue
|
|
178
|
+
}
|
|
179
|
+
alt=""
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<div
|
|
184
|
+
className="image-upload-widget"
|
|
185
|
+
onClick={onFocus}
|
|
186
|
+
onKeyDown={onFocus}
|
|
187
|
+
role="toolbar"
|
|
188
|
+
>
|
|
189
|
+
<Dropzone
|
|
190
|
+
noClick
|
|
191
|
+
onDrop={handleUpload}
|
|
192
|
+
onDragEnter={onDragEnter}
|
|
193
|
+
onDragLeave={onDragLeave}
|
|
194
|
+
className="dropzone"
|
|
195
|
+
>
|
|
196
|
+
{({ getRootProps, getInputProps }) => (
|
|
197
|
+
<div {...getRootProps()}>
|
|
198
|
+
<Message>
|
|
199
|
+
{dragging && <Dimmer active></Dimmer>}
|
|
200
|
+
{uploading && (
|
|
201
|
+
<Dimmer active>
|
|
202
|
+
<Loader indeterminate>
|
|
203
|
+
{intl.formatMessage(messages.uploadingImage)}
|
|
204
|
+
</Loader>
|
|
205
|
+
</Dimmer>
|
|
206
|
+
)}
|
|
207
|
+
<img src={imageBlockSVG} alt="" className="placeholder" />
|
|
208
|
+
<p>{description || intl.formatMessage(messages.addImage)}</p>
|
|
209
|
+
<div className="toolbar-wrapper">
|
|
210
|
+
<div className="toolbar-inner" ref={linkEditor.anchorNode}>
|
|
211
|
+
{hideObjectBrowserPicker === false && (
|
|
212
|
+
<Button.Group>
|
|
213
|
+
<Button
|
|
214
|
+
aria-label={intl.formatMessage(messages.pickAnImage)}
|
|
215
|
+
icon
|
|
216
|
+
basic
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
onFocus && onFocus();
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
openObjectBrowser({
|
|
221
|
+
mode: objectBrowserPickerType,
|
|
222
|
+
onSelectItem: onSelectItem
|
|
223
|
+
? onSelectItem
|
|
224
|
+
: (url, { title, image_field, image_scales }) => {
|
|
225
|
+
onChange(props.id, flattenToAppURL(url), {
|
|
226
|
+
title,
|
|
227
|
+
image_field,
|
|
228
|
+
image_scales,
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
currentPath: contextUrl,
|
|
232
|
+
});
|
|
233
|
+
}}
|
|
234
|
+
type="button"
|
|
235
|
+
>
|
|
236
|
+
<Icon name={navTreeSVG} size="24px" />
|
|
237
|
+
</Button>
|
|
238
|
+
</Button.Group>
|
|
239
|
+
)}
|
|
240
|
+
{restrictFileUpload === false && (
|
|
241
|
+
<Button.Group>
|
|
242
|
+
<Button
|
|
243
|
+
aria-label={intl.formatMessage(messages.uploadAnImage)}
|
|
244
|
+
icon
|
|
245
|
+
basic
|
|
246
|
+
compact
|
|
247
|
+
onClick={() => {
|
|
248
|
+
imageUploadInputRef.current.click();
|
|
249
|
+
}}
|
|
250
|
+
type="button"
|
|
251
|
+
>
|
|
252
|
+
<Icon name={uploadSVG} size="24px" />
|
|
253
|
+
</Button>
|
|
254
|
+
<input
|
|
255
|
+
{...getInputProps({
|
|
256
|
+
type: 'file',
|
|
257
|
+
ref: imageUploadInputRef,
|
|
258
|
+
onChange: handleUpload,
|
|
259
|
+
style: { display: 'none' },
|
|
260
|
+
})}
|
|
261
|
+
/>
|
|
262
|
+
</Button.Group>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{hideLinkPicker === false && (
|
|
266
|
+
<Button.Group>
|
|
267
|
+
<Button
|
|
268
|
+
icon
|
|
269
|
+
basic
|
|
270
|
+
aria-label={intl.formatMessage(messages.linkAnImage)}
|
|
271
|
+
onClick={(e) => {
|
|
272
|
+
!props.selected && onFocus && onFocus();
|
|
273
|
+
linkEditor.show();
|
|
274
|
+
}}
|
|
275
|
+
type="button"
|
|
276
|
+
>
|
|
277
|
+
<Icon name={linkSVG} circled size="24px" />
|
|
278
|
+
</Button>
|
|
279
|
+
</Button.Group>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
{linkEditor.anchorNode && (
|
|
283
|
+
<linkEditor.LinkEditor
|
|
284
|
+
value={imageValue}
|
|
285
|
+
placeholder={
|
|
286
|
+
placeholderLinkInput ||
|
|
287
|
+
intl.formatMessage(messages.linkAnImage)
|
|
288
|
+
}
|
|
289
|
+
objectBrowserPickerType={objectBrowserPickerType}
|
|
290
|
+
onChange={(_, e) =>
|
|
291
|
+
onChange(
|
|
292
|
+
props.id,
|
|
293
|
+
isInternalURL(e) ? flattenToAppURL(e) : e,
|
|
294
|
+
{},
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
id={id}
|
|
298
|
+
/>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
</Message>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</Dropzone>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export const ImageInput = compose(
|
|
310
|
+
connect(
|
|
311
|
+
(state, ownProps) => {
|
|
312
|
+
const requestId = `image-upload-${ownProps.id}`;
|
|
313
|
+
return {
|
|
314
|
+
request: state.content.subrequests[ownProps.block || requestId] || {},
|
|
315
|
+
content: state.content.subrequests[ownProps.block || requestId]?.data,
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
{ createContent },
|
|
319
|
+
),
|
|
320
|
+
)(withObjectBrowser(UnconnectedImageInput));
|
|
321
|
+
|
|
322
|
+
const ImageUploadWidget = (props) => {
|
|
323
|
+
const { fieldSet, id, title } = props;
|
|
324
|
+
return (
|
|
325
|
+
<FormFieldWrapper
|
|
326
|
+
{...props}
|
|
327
|
+
columns={1}
|
|
328
|
+
className="block image-upload-widget"
|
|
329
|
+
>
|
|
330
|
+
<div className="wrapper">
|
|
331
|
+
<label
|
|
332
|
+
id={`fieldset-${fieldSet}-field-label-${id}`}
|
|
333
|
+
htmlFor={`field-${id}`}
|
|
334
|
+
>
|
|
335
|
+
{title}
|
|
336
|
+
</label>
|
|
337
|
+
</div>
|
|
338
|
+
<ImageInput {...props} />
|
|
339
|
+
</FormFieldWrapper>
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export default ImageUploadWidget;
|
|
@@ -170,4 +170,12 @@ defineMessages({
|
|
|
170
170
|
id: 'mainMenu',
|
|
171
171
|
defaultMessage: 'Menù principale',
|
|
172
172
|
},
|
|
173
|
+
ascendingTableSort: {
|
|
174
|
+
id: 'ascendingTableSort',
|
|
175
|
+
defaultMessage: 'ascending',
|
|
176
|
+
},
|
|
177
|
+
descendingTableSort: {
|
|
178
|
+
id: 'descendingTableSort',
|
|
179
|
+
defaultMessage: 'descending',
|
|
180
|
+
},
|
|
173
181
|
});
|