io-sanita-theme 2.31.3 → 2.32.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.32.0](https://github.com/RedTurtle/io-sanita-theme/compare/2.31.3...2.32.0) (2026-06-12)
4
+
5
+ ### Features
6
+
7
+ * aggiunta ricertca per comune/distretto su blocco searchmap ([#152](https://github.com/RedTurtle/io-sanita-theme/issues/152)) ([1aff10f](https://github.com/RedTurtle/io-sanita-theme/commit/1aff10f00963987a00a4acd6aafb754e50d72108))
8
+
9
+ ### Bug Fixes
10
+
11
+ * bottoni categorie searchmap ([#151](https://github.com/RedTurtle/io-sanita-theme/issues/151)) ([058aa59](https://github.com/RedTurtle/io-sanita-theme/commit/058aa59acb0f5d721d653458e38be43f0adf0653))
12
+ * prevent the submit of the form when adding a row o column from the toolbar ([#153](https://github.com/RedTurtle/io-sanita-theme/issues/153)) ([cf723b2](https://github.com/RedTurtle/io-sanita-theme/commit/cf723b2dfd0974d470b40ab54ce3f683f7a558a0))
13
+
14
+ ### Maintenance
15
+
16
+ * prettier ([11b8f22](https://github.com/RedTurtle/io-sanita-theme/commit/11b8f222e8c6d4eb5f93d52069ec3ff2517970b7))
17
+
3
18
  ## [2.31.3](https://github.com/RedTurtle/io-sanita-theme/compare/2.31.2...2.31.3) (2026-06-11)
4
19
 
5
20
  ### Bug Fixes
package/RELEASE.md CHANGED
@@ -41,6 +41,12 @@
41
41
  - ...
42
42
  -->
43
43
 
44
+ ## Versione 2.32.0 (12/06/2026)
45
+
46
+ ### Migliorie
47
+
48
+ - Aggiunta filtri di ricerca per comune e distretto nel blocco "Cerca strutture".
49
+
44
50
  ## Versione 2.31.3 (11/06/2026)
45
51
 
46
52
  ### Fix
@@ -3202,6 +3202,16 @@ msgstr ""
3202
3202
  msgid "search_map_Search path"
3203
3203
  msgstr ""
3204
3204
 
3205
+ #. Default: "Mostra il filtro per comune"
3206
+ #: components/Blocks/SearchMap/schema
3207
+ msgid "search_map_Show city"
3208
+ msgstr ""
3209
+
3210
+ #. Default: "Mostra il filtro per distretto"
3211
+ #: components/Blocks/SearchMap/schema
3212
+ msgid "search_map_Show distretto"
3213
+ msgstr ""
3214
+
3205
3215
  #. Default: "Mostra la barra di ricerca"
3206
3216
  #: components/Blocks/SearchMap/schema
3207
3217
  msgid "search_map_Show search bar"
@@ -3222,6 +3232,16 @@ msgstr ""
3222
3232
  msgid "search_map_ct"
3223
3233
  msgstr ""
3224
3234
 
3235
+ #. Default: "Comune"
3236
+ #: components/Blocks/SearchMap/Body
3237
+ msgid "search_map_label_city"
3238
+ msgstr ""
3239
+
3240
+ #. Default: "Distretto"
3241
+ #: components/Blocks/SearchMap/Body
3242
+ msgid "search_map_label_distretto"
3243
+ msgstr ""
3244
+
3225
3245
  #. Default: "Cerca"
3226
3246
  #: components/Widgets/SearchBar/SearchBar
3227
3247
  msgid "search_map_searchable_text_button"
@@ -3247,6 +3267,16 @@ msgstr ""
3247
3267
  msgid "search_map_title"
3248
3268
  msgstr ""
3249
3269
 
3270
+ #. Default: "Tutti i comuni"
3271
+ #: components/Blocks/SearchMap/Body
3272
+ msgid "search_mapall_cities"
3273
+ msgstr ""
3274
+
3275
+ #. Default: "Tutti i distretti"
3276
+ #: components/Blocks/SearchMap/Body
3277
+ msgid "search_mapall_distretti"
3278
+ msgstr ""
3279
+
3250
3280
  #. Default: "Tutti"
3251
3281
  #: components/Blocks/SearchMap/Body
3252
3282
  msgid "search_mapall_subjects"
@@ -3197,6 +3197,16 @@ msgstr "Users"
3197
3197
  msgid "search_map_Search path"
3198
3198
  msgstr ""
3199
3199
 
3200
+ #. Default: "Mostra il filtro per comune"
3201
+ #: components/Blocks/SearchMap/schema
3202
+ msgid "search_map_Show city"
3203
+ msgstr ""
3204
+
3205
+ #. Default: "Mostra il filtro per distretto"
3206
+ #: components/Blocks/SearchMap/schema
3207
+ msgid "search_map_Show distretto"
3208
+ msgstr ""
3209
+
3200
3210
  #. Default: "Mostra la barra di ricerca"
3201
3211
  #: components/Blocks/SearchMap/schema
3202
3212
  msgid "search_map_Show search bar"
@@ -3217,6 +3227,16 @@ msgstr ""
3217
3227
  msgid "search_map_ct"
3218
3228
  msgstr ""
3219
3229
 
3230
+ #. Default: "Comune"
3231
+ #: components/Blocks/SearchMap/Body
3232
+ msgid "search_map_label_city"
3233
+ msgstr "Municipality"
3234
+
3235
+ #. Default: "Distretto"
3236
+ #: components/Blocks/SearchMap/Body
3237
+ msgid "search_map_label_distretto"
3238
+ msgstr "District"
3239
+
3220
3240
  #. Default: "Cerca"
3221
3241
  #: components/Widgets/SearchBar/SearchBar
3222
3242
  msgid "search_map_searchable_text_button"
@@ -3242,6 +3262,16 @@ msgstr "Search for facilities near you"
3242
3262
  msgid "search_map_title"
3243
3263
  msgstr ""
3244
3264
 
3265
+ #. Default: "Tutti i comuni"
3266
+ #: components/Blocks/SearchMap/Body
3267
+ msgid "search_mapall_cities"
3268
+ msgstr "All municipalities"
3269
+
3270
+ #. Default: "Tutti i distretti"
3271
+ #: components/Blocks/SearchMap/Body
3272
+ msgid "search_mapall_distretti"
3273
+ msgstr "All districts"
3274
+
3245
3275
  #. Default: "Tutti"
3246
3276
  #: components/Blocks/SearchMap/Body
3247
3277
  msgid "search_mapall_subjects"
@@ -3204,6 +3204,16 @@ msgstr ""
3204
3204
  msgid "search_map_Search path"
3205
3205
  msgstr ""
3206
3206
 
3207
+ #. Default: "Mostra il filtro per comune"
3208
+ #: components/Blocks/SearchMap/schema
3209
+ msgid "search_map_Show city"
3210
+ msgstr ""
3211
+
3212
+ #. Default: "Mostra il filtro per distretto"
3213
+ #: components/Blocks/SearchMap/schema
3214
+ msgid "search_map_Show distretto"
3215
+ msgstr ""
3216
+
3207
3217
  #. Default: "Mostra la barra di ricerca"
3208
3218
  #: components/Blocks/SearchMap/schema
3209
3219
  msgid "search_map_Show search bar"
@@ -3224,6 +3234,16 @@ msgstr ""
3224
3234
  msgid "search_map_ct"
3225
3235
  msgstr ""
3226
3236
 
3237
+ #. Default: "Comune"
3238
+ #: components/Blocks/SearchMap/Body
3239
+ msgid "search_map_label_city"
3240
+ msgstr ""
3241
+
3242
+ #. Default: "Distretto"
3243
+ #: components/Blocks/SearchMap/Body
3244
+ msgid "search_map_label_distretto"
3245
+ msgstr ""
3246
+
3227
3247
  #. Default: "Cerca"
3228
3248
  #: components/Widgets/SearchBar/SearchBar
3229
3249
  msgid "search_map_searchable_text_button"
@@ -3249,6 +3269,16 @@ msgstr ""
3249
3269
  msgid "search_map_title"
3250
3270
  msgstr ""
3251
3271
 
3272
+ #. Default: "Tutti i comuni"
3273
+ #: components/Blocks/SearchMap/Body
3274
+ msgid "search_mapall_cities"
3275
+ msgstr ""
3276
+
3277
+ #. Default: "Tutti i distretti"
3278
+ #: components/Blocks/SearchMap/Body
3279
+ msgid "search_mapall_distretti"
3280
+ msgstr ""
3281
+
3252
3282
  #. Default: "Tutti"
3253
3283
  #: components/Blocks/SearchMap/Body
3254
3284
  msgid "search_mapall_subjects"
@@ -3204,6 +3204,16 @@ msgstr ""
3204
3204
  msgid "search_map_Search path"
3205
3205
  msgstr ""
3206
3206
 
3207
+ #. Default: "Mostra il filtro per comune"
3208
+ #: components/Blocks/SearchMap/schema
3209
+ msgid "search_map_Show city"
3210
+ msgstr ""
3211
+
3212
+ #. Default: "Mostra il filtro per distretto"
3213
+ #: components/Blocks/SearchMap/schema
3214
+ msgid "search_map_Show distretto"
3215
+ msgstr ""
3216
+
3207
3217
  #. Default: "Mostra la barra di ricerca"
3208
3218
  #: components/Blocks/SearchMap/schema
3209
3219
  msgid "search_map_Show search bar"
@@ -3224,6 +3234,16 @@ msgstr ""
3224
3234
  msgid "search_map_ct"
3225
3235
  msgstr ""
3226
3236
 
3237
+ #. Default: "Comune"
3238
+ #: components/Blocks/SearchMap/Body
3239
+ msgid "search_map_label_city"
3240
+ msgstr ""
3241
+
3242
+ #. Default: "Distretto"
3243
+ #: components/Blocks/SearchMap/Body
3244
+ msgid "search_map_label_distretto"
3245
+ msgstr ""
3246
+
3227
3247
  #. Default: "Cerca"
3228
3248
  #: components/Widgets/SearchBar/SearchBar
3229
3249
  msgid "search_map_searchable_text_button"
@@ -3249,6 +3269,16 @@ msgstr ""
3249
3269
  msgid "search_map_title"
3250
3270
  msgstr ""
3251
3271
 
3272
+ #. Default: "Tutti i comuni"
3273
+ #: components/Blocks/SearchMap/Body
3274
+ msgid "search_mapall_cities"
3275
+ msgstr ""
3276
+
3277
+ #. Default: "Tutti i distretti"
3278
+ #: components/Blocks/SearchMap/Body
3279
+ msgid "search_mapall_distretti"
3280
+ msgstr ""
3281
+
3252
3282
  #. Default: "Tutti"
3253
3283
  #: components/Blocks/SearchMap/Body
3254
3284
  msgid "search_mapall_subjects"
@@ -3197,6 +3197,16 @@ msgstr ""
3197
3197
  msgid "search_map_Search path"
3198
3198
  msgstr ""
3199
3199
 
3200
+ #. Default: "Mostra il filtro per comune"
3201
+ #: components/Blocks/SearchMap/schema
3202
+ msgid "search_map_Show city"
3203
+ msgstr ""
3204
+
3205
+ #. Default: "Mostra il filtro per distretto"
3206
+ #: components/Blocks/SearchMap/schema
3207
+ msgid "search_map_Show distretto"
3208
+ msgstr ""
3209
+
3200
3210
  #. Default: "Mostra la barra di ricerca"
3201
3211
  #: components/Blocks/SearchMap/schema
3202
3212
  msgid "search_map_Show search bar"
@@ -3217,6 +3227,16 @@ msgstr ""
3217
3227
  msgid "search_map_ct"
3218
3228
  msgstr ""
3219
3229
 
3230
+ #. Default: "Comune"
3231
+ #: components/Blocks/SearchMap/Body
3232
+ msgid "search_map_label_city"
3233
+ msgstr ""
3234
+
3235
+ #. Default: "Distretto"
3236
+ #: components/Blocks/SearchMap/Body
3237
+ msgid "search_map_label_distretto"
3238
+ msgstr ""
3239
+
3220
3240
  #. Default: "Cerca"
3221
3241
  #: components/Widgets/SearchBar/SearchBar
3222
3242
  msgid "search_map_searchable_text_button"
@@ -3242,6 +3262,16 @@ msgstr ""
3242
3262
  msgid "search_map_title"
3243
3263
  msgstr ""
3244
3264
 
3265
+ #. Default: "Tutti i comuni"
3266
+ #: components/Blocks/SearchMap/Body
3267
+ msgid "search_mapall_cities"
3268
+ msgstr ""
3269
+
3270
+ #. Default: "Tutti i distretti"
3271
+ #: components/Blocks/SearchMap/Body
3272
+ msgid "search_mapall_distretti"
3273
+ msgstr ""
3274
+
3245
3275
  #. Default: "Tutti"
3246
3276
  #: components/Blocks/SearchMap/Body
3247
3277
  msgid "search_mapall_subjects"
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: 2026-06-11T07:39:40.656Z\n"
4
+ "POT-Creation-Date: 2026-06-12T14:38:11.948Z\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"
@@ -3199,6 +3199,16 @@ msgstr ""
3199
3199
  msgid "search_map_Search path"
3200
3200
  msgstr ""
3201
3201
 
3202
+ #. Default: "Mostra il filtro per comune"
3203
+ #: components/Blocks/SearchMap/schema
3204
+ msgid "search_map_Show city"
3205
+ msgstr ""
3206
+
3207
+ #. Default: "Mostra il filtro per distretto"
3208
+ #: components/Blocks/SearchMap/schema
3209
+ msgid "search_map_Show distretto"
3210
+ msgstr ""
3211
+
3202
3212
  #. Default: "Mostra la barra di ricerca"
3203
3213
  #: components/Blocks/SearchMap/schema
3204
3214
  msgid "search_map_Show search bar"
@@ -3219,6 +3229,16 @@ msgstr ""
3219
3229
  msgid "search_map_ct"
3220
3230
  msgstr ""
3221
3231
 
3232
+ #. Default: "Comune"
3233
+ #: components/Blocks/SearchMap/Body
3234
+ msgid "search_map_label_city"
3235
+ msgstr ""
3236
+
3237
+ #. Default: "Distretto"
3238
+ #: components/Blocks/SearchMap/Body
3239
+ msgid "search_map_label_distretto"
3240
+ msgstr ""
3241
+
3222
3242
  #. Default: "Cerca"
3223
3243
  #: components/Widgets/SearchBar/SearchBar
3224
3244
  msgid "search_map_searchable_text_button"
@@ -3244,6 +3264,16 @@ msgstr ""
3244
3264
  msgid "search_map_title"
3245
3265
  msgstr ""
3246
3266
 
3267
+ #. Default: "Tutti i comuni"
3268
+ #: components/Blocks/SearchMap/Body
3269
+ msgid "search_mapall_cities"
3270
+ msgstr ""
3271
+
3272
+ #. Default: "Tutti i distretti"
3273
+ #: components/Blocks/SearchMap/Body
3274
+ msgid "search_mapall_distretti"
3275
+ msgstr ""
3276
+
3247
3277
  #. Default: "Tutti"
3248
3278
  #: components/Blocks/SearchMap/Body
3249
3279
  msgid "search_mapall_subjects"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "io-sanita-theme",
3
- "version": "2.31.3",
3
+ "version": "2.32.0",
4
4
  "description": "io-sanita-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
@@ -70,6 +70,22 @@ const messages = defineMessages({
70
70
  id: 'search_mapremove_all_subjects',
71
71
  defaultMessage: 'Mostra tutte le categorie',
72
72
  },
73
+ all_cities: {
74
+ id: 'search_mapall_cities',
75
+ defaultMessage: 'Tutti i comuni',
76
+ },
77
+ all_distretti: {
78
+ id: 'search_mapall_distretti',
79
+ defaultMessage: 'Tutti i distretti',
80
+ },
81
+ label_city: {
82
+ id: 'search_map_label_city',
83
+ defaultMessage: 'Comune',
84
+ },
85
+ label_distretto: {
86
+ id: 'search_map_label_distretto',
87
+ defaultMessage: 'Distretto',
88
+ },
73
89
  });
74
90
 
75
91
  const LeafIcon = (options, item) => {
@@ -128,9 +144,13 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
128
144
  const [filters, setFilters] = useState({
129
145
  searchableText: '',
130
146
  subjects: new Set(),
147
+ city: '',
148
+ distretto: '',
131
149
  });
132
150
  const [markers, setMarkers] = useState([]);
133
151
  const [subjects, setSubjects] = useState(new Set());
152
+ const [cities, setCities] = useState(new Set());
153
+ const [distretti, setDistretti] = useState(new Set());
134
154
  const [rendered_items, setRenderedItems] = useState([]);
135
155
 
136
156
  const querystringResults = useSelector((state) => {
@@ -146,6 +166,8 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
146
166
  const doSearch = () => {
147
167
  //default filtes
148
168
  setSubjects(new Set());
169
+ setCities(new Set());
170
+ setDistretti(new Set());
149
171
  setCurrentPage(1);
150
172
 
151
173
  let query = [
@@ -195,6 +217,22 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
195
217
  });
196
218
  }
197
219
 
220
+ if (filters.city) {
221
+ query.push({
222
+ i: 'city',
223
+ o: 'plone.app.querystring.operation.selection.is',
224
+ v: [filters.city],
225
+ });
226
+ }
227
+
228
+ if (filters.distretto) {
229
+ query.push({
230
+ i: 'distretto',
231
+ o: 'plone.app.querystring.operation.selection.is',
232
+ v: [filters.distretto],
233
+ });
234
+ }
235
+
198
236
  dispatch(
199
237
  getQueryStringResults(
200
238
  subsite ? flattenToAppURL(subsite['@id']) : '',
@@ -204,6 +242,8 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
204
242
  'struttura_ricevimento',
205
243
  'struttura_in_cui_opera',
206
244
  'incarico_metadata',
245
+ 'city',
246
+ 'distretto',
207
247
  ], //'_all',
208
248
  query: query,
209
249
  b_size: b_size,
@@ -284,6 +324,26 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
284
324
  }
285
325
  };
286
326
 
327
+ const calculateCities = () => {
328
+ if (cities.size === 0) {
329
+ let vals = new Set();
330
+ items.forEach((item) => {
331
+ if (item.city) vals.add(item.city);
332
+ });
333
+ setCities(vals);
334
+ }
335
+ };
336
+
337
+ const calculateDistretti = () => {
338
+ if (distretti.size === 0) {
339
+ let vals = new Set();
340
+ items.forEach((item) => {
341
+ if (item.distretto) vals.add(item.distretto);
342
+ });
343
+ setDistretti(vals);
344
+ }
345
+ };
346
+
287
347
  useEffect(() => {
288
348
  setItems(resultsReducer(resultItems));
289
349
  }, [resultItems]);
@@ -291,6 +351,8 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
291
351
  useEffect(() => {
292
352
  calculateMarkers();
293
353
  calculateSubjects();
354
+ calculateCities();
355
+ calculateDistretti();
294
356
  }, [items]);
295
357
 
296
358
  useEffect(() => {
@@ -372,6 +434,71 @@ const SearchMapBody = ({ data, id, path, properties, block, inEditMode }) => {
372
434
  </Col>
373
435
  </Row>
374
436
  )}
437
+ {((data.show_city && cities.size > 0) ||
438
+ (data.show_distretto && distretti.size > 0)) && (
439
+ <Row className="pb-3 g-2">
440
+ {data.show_city && cities.size > 0 && (
441
+ <Col xs="auto">
442
+ <label
443
+ htmlFor={block_id + '-city-select'}
444
+ className="form-label"
445
+ >
446
+ {intl.formatMessage(messages.label_city)}
447
+ </label>
448
+ <select
449
+ id={block_id + '-city-select'}
450
+ className="form-select"
451
+ value={filters.city}
452
+ aria-controls={results_region_id}
453
+ onChange={(e) =>
454
+ setFilters({ ...filters, city: e.target.value })
455
+ }
456
+ >
457
+ <option value="">
458
+ {intl.formatMessage(messages.all_cities)}
459
+ </option>
460
+ {[...cities].sort().map((c) => (
461
+ <option key={c} value={c}>
462
+ {c}
463
+ </option>
464
+ ))}
465
+ </select>
466
+ </Col>
467
+ )}
468
+ {data.show_distretto && distretti.size > 0 && (
469
+ <Col xs="auto">
470
+ <label
471
+ htmlFor={block_id + '-distretto-select'}
472
+ className="form-label"
473
+ >
474
+ {intl.formatMessage(messages.label_distretto)}
475
+ </label>
476
+ <select
477
+ id={block_id + '-distretto-select'}
478
+ className="form-select"
479
+ value={filters.distretto}
480
+ aria-controls={results_region_id}
481
+ onChange={(e) =>
482
+ setFilters({
483
+ ...filters,
484
+ distretto: e.target.value,
485
+ })
486
+ }
487
+ >
488
+ <option value="">
489
+ {intl.formatMessage(messages.all_distretti)}
490
+ </option>
491
+ {[...distretti].sort().map((d) => (
492
+ <option key={d} value={d}>
493
+ {d}
494
+ </option>
495
+ ))}
496
+ </select>
497
+ </Col>
498
+ )}
499
+ </Row>
500
+ )}
501
+
375
502
  {data.show_types && subjects.size > 0 && (
376
503
  <div className="subjects pb-3">
377
504
  {/*Chip 'tutti'*/}
@@ -17,6 +17,14 @@ const messages = defineMessages({
17
17
  id: 'search_map_Show types',
18
18
  defaultMessage: 'Mostra i filtri per tipologia',
19
19
  },
20
+ show_city: {
21
+ id: 'search_map_Show city',
22
+ defaultMessage: 'Mostra il filtro per comune',
23
+ },
24
+ show_distretto: {
25
+ id: 'search_map_Show distretto',
26
+ defaultMessage: 'Mostra il filtro per distretto',
27
+ },
20
28
  portal_type: {
21
29
  id: 'search_map_ct',
22
30
  defaultMessage: 'Tipo di contenuto',
@@ -39,7 +47,9 @@ export function SearchMapSchema({ formData, intl }) {
39
47
  'path',
40
48
  'portal_type',
41
49
  'show_search_bar',
42
- ...(formData.portal_type === 'Struttura' ? ['show_types'] : []),
50
+ ...(formData.portal_type === 'Struttura'
51
+ ? ['show_types', 'show_city', 'show_distretto']
52
+ : []),
43
53
  ],
44
54
  },
45
55
  ],
@@ -71,6 +81,14 @@ export function SearchMapSchema({ formData, intl }) {
71
81
  title: intl.formatMessage(messages.show_types),
72
82
  type: 'boolean',
73
83
  },
84
+ show_city: {
85
+ title: intl.formatMessage(messages.show_city),
86
+ type: 'boolean',
87
+ },
88
+ show_distretto: {
89
+ title: intl.formatMessage(messages.show_distretto),
90
+ type: 'boolean',
91
+ },
74
92
  },
75
93
  required: [],
76
94
  };
@@ -9,6 +9,7 @@
9
9
  .subjects {
10
10
  display: flex;
11
11
  overflow: scroll;
12
+ flex-wrap: wrap;
12
13
  button {
13
14
  flex: 0 0 auto;
14
15
  }
@@ -0,0 +1,548 @@
1
+ /**
2
+ * Slate Table block editor.
3
+ * @module volto-slate/blocks/Table/Edit
4
+ */
5
+
6
+ // Customizations:
7
+ // prevent the submit of the form when adding a row o column from the toolbar
8
+
9
+ import React, { Component } from 'react';
10
+ import PropTypes from 'prop-types';
11
+ import isEmpty from 'lodash/isEmpty';
12
+ import map from 'lodash/map';
13
+ import remove from 'lodash/remove';
14
+ import { Button, Table } from 'semantic-ui-react';
15
+ import cx from 'classnames';
16
+ import { defineMessages, injectIntl } from 'react-intl';
17
+
18
+ import Cell from '@plone/volto-slate/blocks/Table/Cell.jsx';
19
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
20
+ import SidebarPortal from '@plone/volto/components/manage/Sidebar/SidebarPortal';
21
+ import { BlockDataForm } from '@plone/volto/components/manage/Form';
22
+ import TableSchema from '@plone/volto-slate/blocks/Table//schema.js';
23
+
24
+ import rowBeforeSVG from '@plone/volto/icons/row-before.svg';
25
+ import rowAfterSVG from '@plone/volto/icons/row-after.svg';
26
+ import colBeforeSVG from '@plone/volto/icons/column-before.svg';
27
+ import colAfterSVG from '@plone/volto/icons/column-after.svg';
28
+ import rowDeleteSVG from '@plone/volto/icons/row-delete.svg';
29
+ import colDeleteSVG from '@plone/volto/icons/column-delete.svg';
30
+
31
+ const getId = () => Math.floor(Math.random() * Math.pow(2, 24)).toString(32);
32
+
33
+ function getEmptyParagraph() {
34
+ return [{ type: 'p', children: [{ text: '' }] }];
35
+ }
36
+
37
+ const emptyCell = (type = 'data') => ({
38
+ key: getId(),
39
+ type: type,
40
+ value: getEmptyParagraph(),
41
+ });
42
+
43
+ const emptyRow = (cells) => ({
44
+ key: getId(),
45
+ cells: map(cells, () => emptyCell()),
46
+ });
47
+
48
+ const initialTable = {
49
+ hideHeaders: false,
50
+ fixed: true,
51
+ compact: false,
52
+ basic: false,
53
+ celled: true,
54
+ inverted: false,
55
+ striped: false,
56
+ rows: [
57
+ {
58
+ key: getId(),
59
+ cells: [
60
+ { key: getId(), type: 'header', value: getEmptyParagraph() },
61
+ { key: getId(), type: 'header', value: getEmptyParagraph() },
62
+ ],
63
+ },
64
+ {
65
+ key: getId(),
66
+ cells: [
67
+ { key: getId(), type: 'data', value: getEmptyParagraph() },
68
+ { key: getId(), type: 'data', value: getEmptyParagraph() },
69
+ ],
70
+ },
71
+ ],
72
+ };
73
+
74
+ const messages = defineMessages({
75
+ insertRowBefore: {
76
+ id: 'Insert row before',
77
+ defaultMessage: 'Insert row before',
78
+ },
79
+ insertRowAfter: {
80
+ id: 'Insert row after',
81
+ defaultMessage: 'Insert row after',
82
+ },
83
+ deleteRow: {
84
+ id: 'Delete row',
85
+ defaultMessage: 'Delete row',
86
+ },
87
+ insertColBefore: {
88
+ id: 'Insert col before',
89
+ defaultMessage: 'Insert col before',
90
+ },
91
+ insertColAfter: {
92
+ id: 'Insert col after',
93
+ defaultMessage: 'Insert col after',
94
+ },
95
+ deleteCol: {
96
+ id: 'Delete col',
97
+ defaultMessage: 'Delete col',
98
+ },
99
+ left: { id: 'Left', defaultMessage: 'Left' },
100
+ center: { id: 'Center', defaultMessage: 'Center' },
101
+ right: { id: 'Right', defaultMessage: 'Right' },
102
+ bottom: { id: 'Bottom', defaultMessage: 'Bottom' },
103
+ middle: { id: 'Middle', defaultMessage: 'Middle' },
104
+ top: { id: 'Top', defaultMessage: 'Top' },
105
+ });
106
+
107
+ class Edit extends Component {
108
+ static propTypes = {
109
+ data: PropTypes.objectOf(PropTypes.any).isRequired,
110
+ detached: PropTypes.bool,
111
+ index: PropTypes.number.isRequired,
112
+ selected: PropTypes.bool.isRequired,
113
+ block: PropTypes.string.isRequired,
114
+ onAddBlock: PropTypes.func.isRequired,
115
+ onChangeBlock: PropTypes.func.isRequired,
116
+ onDeleteBlock: PropTypes.func.isRequired,
117
+ onInsertBlock: PropTypes.func.isRequired,
118
+ onMutateBlock: PropTypes.func.isRequired,
119
+ onFocusPreviousBlock: PropTypes.func.isRequired,
120
+ onFocusNextBlock: PropTypes.func.isRequired,
121
+ onSelectBlock: PropTypes.func.isRequired,
122
+ };
123
+
124
+ static defaultProps = {
125
+ detached: false,
126
+ };
127
+
128
+ constructor(props) {
129
+ super(props);
130
+ this.state = {
131
+ headers: [],
132
+ rows: {},
133
+ selected: { row: 0, cell: 0 },
134
+ isClient: false,
135
+ };
136
+ this.onChange = this.onChange.bind(this);
137
+ this.onSelectCell = this.onSelectCell.bind(this);
138
+ this.onInsertRowBefore = this.onInsertRowBefore.bind(this);
139
+ this.onInsertRowAfter = this.onInsertRowAfter.bind(this);
140
+ this.onInsertColBefore = this.onInsertColBefore.bind(this);
141
+ this.onInsertColAfter = this.onInsertColAfter.bind(this);
142
+ this.onDeleteRow = this.onDeleteRow.bind(this);
143
+ this.onDeleteCol = this.onDeleteCol.bind(this);
144
+ this.onChangeCell = this.onChangeCell.bind(this);
145
+ this.toggleCellType = this.toggleCellType.bind(this);
146
+ }
147
+
148
+ componentDidMount() {
149
+ if (!this.props.data.table || isEmpty(this.props.data.table)) {
150
+ this.props.onChangeBlock(this.props.block, {
151
+ ...this.props.data,
152
+ table: initialTable,
153
+ });
154
+ }
155
+ this.setState({ isClient: true });
156
+ }
157
+
158
+ UNSAFE_componentWillReceiveProps(nextProps) {
159
+ if (!nextProps.data.table || isEmpty(nextProps.data.table)) {
160
+ this.props.onChangeBlock(nextProps.block, {
161
+ ...nextProps.data,
162
+ table: initialTable,
163
+ });
164
+ }
165
+ }
166
+
167
+ onChange(id, value) {
168
+ const table = this.props.data.table;
169
+ this.props.onChangeBlock(this.props.block, {
170
+ ...this.props.data,
171
+ table: { ...table, [id]: value },
172
+ });
173
+ }
174
+
175
+ onSelectCell(row, cell) {
176
+ this.setState({ selected: { row, cell } });
177
+ }
178
+
179
+ onChangeCell(row, cell, slateValue) {
180
+ const table = JSON.parse(JSON.stringify(this.props.data.table));
181
+ table.rows[row].cells[cell] = {
182
+ ...table.rows[row].cells[cell],
183
+ value: JSON.parse(JSON.stringify(slateValue)),
184
+ };
185
+ this.props.onChangeBlock(this.props.block, {
186
+ ...this.props.data,
187
+ table,
188
+ });
189
+ }
190
+
191
+ toggleCellType() {
192
+ const table = { ...this.props.data.table };
193
+ let type =
194
+ table.rows[this.state.selected.row].cells[this.state.selected.cell].type;
195
+ table.rows[this.state.selected.row].cells[this.state.selected.cell].type =
196
+ type === 'header' ? 'data' : 'header';
197
+ this.props.onChangeBlock(this.props.block, {
198
+ ...this.props.data,
199
+ table,
200
+ });
201
+ }
202
+
203
+ onInsertRowBefore(ev) {
204
+ ev.preventDefault();
205
+ ev.stopPropagation();
206
+ const table = this.props.data.table;
207
+ this.props.onChangeBlock(this.props.block, {
208
+ ...this.props.data,
209
+ table: {
210
+ ...table,
211
+ rows: [
212
+ ...table.rows.slice(0, this.state.selected.row),
213
+ emptyRow(table.rows[0].cells),
214
+ ...table.rows.slice(this.state.selected.row),
215
+ ],
216
+ },
217
+ });
218
+ this.setState({
219
+ selected: {
220
+ row: this.state.selected.row + 1,
221
+ cell: this.state.selected.cell,
222
+ },
223
+ });
224
+ }
225
+
226
+ onInsertRowAfter(ev) {
227
+ ev.preventDefault();
228
+ ev.stopPropagation();
229
+ const table = this.props.data.table;
230
+ this.props.onChangeBlock(this.props.block, {
231
+ ...this.props.data,
232
+ table: {
233
+ ...table,
234
+ rows: [
235
+ ...table.rows.slice(0, this.state.selected.row + 1),
236
+ emptyRow(table.rows[0].cells),
237
+ ...table.rows.slice(this.state.selected.row + 1),
238
+ ],
239
+ },
240
+ });
241
+ }
242
+
243
+ onInsertColBefore(ev) {
244
+ ev.preventDefault();
245
+ ev.stopPropagation();
246
+ const table = this.props.data.table;
247
+ this.props.onChangeBlock(this.props.block, {
248
+ ...this.props.data,
249
+ table: {
250
+ ...table,
251
+ rows: map(table.rows, (row, index) => ({
252
+ ...row,
253
+ cells: [
254
+ ...row.cells.slice(0, this.state.selected.cell),
255
+ emptyCell(table.rows[index].cells[this.state.selected.cell].type),
256
+ ...row.cells.slice(this.state.selected.cell),
257
+ ],
258
+ })),
259
+ },
260
+ });
261
+ this.setState({
262
+ selected: {
263
+ row: this.state.selected.row,
264
+ cell: this.state.selected.cell + 1,
265
+ },
266
+ });
267
+ }
268
+
269
+ onInsertColAfter(ev) {
270
+ ev.preventDefault();
271
+ ev.stopPropagation();
272
+ const table = this.props.data.table;
273
+ this.props.onChangeBlock(this.props.block, {
274
+ ...this.props.data,
275
+ table: {
276
+ ...table,
277
+ rows: map(table.rows, (row, index) => ({
278
+ ...row,
279
+ cells: [
280
+ ...row.cells.slice(0, this.state.selected.cell + 1),
281
+ emptyCell(table.rows[index].cells[this.state.selected.cell].type),
282
+ ...row.cells.slice(this.state.selected.cell + 1),
283
+ ],
284
+ })),
285
+ },
286
+ });
287
+ }
288
+
289
+ onDeleteCol(ev) {
290
+ ev.preventDefault();
291
+ ev.stopPropagation();
292
+ const table = this.props.data.table;
293
+
294
+ if (this.state.selected.cell === table.rows[0].cells.length - 1) {
295
+ this.setState({
296
+ selected: {
297
+ row: this.state.selected.row,
298
+ cell: this.state.selected.cell - 1,
299
+ },
300
+ });
301
+ }
302
+
303
+ this.props.onChangeBlock(this.props.block, {
304
+ ...this.props.data,
305
+ table: {
306
+ ...table,
307
+ rows: map(table.rows, (row) => ({
308
+ ...row,
309
+ cells: remove(
310
+ row.cells,
311
+ (cell, index) => index !== this.state.selected.cell,
312
+ ),
313
+ })),
314
+ },
315
+ });
316
+ }
317
+
318
+ onDeleteRow(ev) {
319
+ ev.preventDefault();
320
+ ev.stopPropagation();
321
+ const table = this.props.data.table;
322
+
323
+ if (this.state.selected.row === table.rows.length - 1) {
324
+ this.setState({
325
+ selected: {
326
+ row: this.state.selected.row - 1,
327
+ cell: this.state.selected.cell,
328
+ },
329
+ });
330
+ }
331
+
332
+ this.props.onChangeBlock(this.props.block, {
333
+ ...this.props.data,
334
+ table: {
335
+ ...table,
336
+ rows: remove(
337
+ table.rows,
338
+ (row, index) => index !== this.state.selected.row,
339
+ ),
340
+ },
341
+ });
342
+ }
343
+
344
+ componentDidUpdate(prevProps) {
345
+ if (prevProps.selected && !this.props.selected) {
346
+ this.setState({ selected: null });
347
+ }
348
+ }
349
+
350
+ render() {
351
+ const headers = this.props.data.table?.rows?.[0]?.cells || [];
352
+ const rows =
353
+ this.props.data.table?.rows?.filter((_, index) => index > 0) || [];
354
+ const schema = TableSchema(this.props);
355
+
356
+ return (
357
+ <div className={cx('block table', { selected: this.props.selected })}>
358
+ {this.props.selected && (
359
+ <div className="toolbar">
360
+ <Button.Group>
361
+ <Button
362
+ icon
363
+ basic
364
+ onClick={this.onInsertRowBefore}
365
+ title={this.props.intl.formatMessage(messages.insertRowBefore)}
366
+ aria-label={this.props.intl.formatMessage(
367
+ messages.insertRowBefore,
368
+ )}
369
+ >
370
+ <Icon name={rowBeforeSVG} size="24px" />
371
+ </Button>
372
+ </Button.Group>
373
+ <Button.Group>
374
+ <Button
375
+ icon
376
+ basic
377
+ onClick={this.onInsertRowAfter}
378
+ title={this.props.intl.formatMessage(messages.insertRowAfter)}
379
+ aria-label={this.props.intl.formatMessage(
380
+ messages.insertRowAfter,
381
+ )}
382
+ >
383
+ <Icon name={rowAfterSVG} size="24px" />
384
+ </Button>
385
+ </Button.Group>
386
+ <Button.Group>
387
+ <Button
388
+ icon
389
+ basic
390
+ onClick={this.onDeleteRow}
391
+ disabled={this.props.data.table?.rows?.length === 1}
392
+ title={this.props.intl.formatMessage(messages.deleteRow)}
393
+ aria-label={this.props.intl.formatMessage(messages.deleteRow)}
394
+ >
395
+ <Icon name={rowDeleteSVG} size="24px" />
396
+ </Button>
397
+ </Button.Group>
398
+ <Button.Group>
399
+ <Button
400
+ icon
401
+ basic
402
+ onClick={this.onInsertColBefore}
403
+ title={this.props.intl.formatMessage(messages.insertColBefore)}
404
+ aria-label={this.props.intl.formatMessage(
405
+ messages.insertColBefore,
406
+ )}
407
+ >
408
+ <Icon name={colBeforeSVG} size="24px" />
409
+ </Button>
410
+ </Button.Group>
411
+ <Button.Group>
412
+ <Button
413
+ icon
414
+ basic
415
+ onClick={this.onInsertColAfter}
416
+ title={this.props.intl.formatMessage(messages.insertColAfter)}
417
+ aria-label={this.props.intl.formatMessage(
418
+ messages.insertColAfter,
419
+ )}
420
+ >
421
+ <Icon name={colAfterSVG} size="24px" />
422
+ </Button>
423
+ </Button.Group>
424
+ <Button.Group>
425
+ <Button
426
+ icon
427
+ basic
428
+ onClick={this.onDeleteCol}
429
+ disabled={this.props.data.table?.rows?.[0].cells.length === 1}
430
+ title={this.props.intl.formatMessage(messages.deleteCol)}
431
+ aria-label={this.props.intl.formatMessage(messages.deleteCol)}
432
+ >
433
+ <Icon name={colDeleteSVG} size="24px" />
434
+ </Button>
435
+ </Button.Group>
436
+ </div>
437
+ )}
438
+ {this.props.data.table && (
439
+ <Table
440
+ fixed={this.props.data.table.fixed}
441
+ compact={this.props.data.table.compact}
442
+ basic={this.props.data.table.basic ? 'very' : false}
443
+ celled={this.props.data.table.celled}
444
+ inverted={this.props.data.table.inverted}
445
+ striped={this.props.data.table.striped}
446
+ className="slate-table-block"
447
+ >
448
+ {!this.props.data.table.hideHeaders ? (
449
+ <Table.Header>
450
+ <Table.Row textAlign="left">
451
+ {headers.map((cell, cellIndex) => (
452
+ <Table.HeaderCell
453
+ key={cell.key}
454
+ textAlign="left"
455
+ verticalAlign="middle"
456
+ >
457
+ <Cell
458
+ value={cell.value}
459
+ row={0}
460
+ cell={cellIndex}
461
+ onSelectCell={this.onSelectCell}
462
+ selected={
463
+ this.props.selected &&
464
+ this.state.selected &&
465
+ 0 === this.state.selected.row &&
466
+ cellIndex === this.state.selected.cell
467
+ }
468
+ selectedCell={this.state.selected}
469
+ isTableBlockSelected={this.props.selected}
470
+ onAddBlock={this.props.onAddBlock}
471
+ onSelectBlock={this.props.onSelectBlock}
472
+ onChange={this.onChangeCell}
473
+ index={this.props.index}
474
+ />
475
+ </Table.HeaderCell>
476
+ ))}
477
+ </Table.Row>
478
+ </Table.Header>
479
+ ) : (
480
+ ''
481
+ )}
482
+ <Table.Body>
483
+ {map(rows, (row, rowIndex) => (
484
+ <Table.Row key={row.key}>
485
+ {map(row.cells, (cell, cellIndex) => (
486
+ <Table.Cell
487
+ key={cell.key}
488
+ textAlign="left"
489
+ verticalAlign="middle"
490
+ className={
491
+ this.props.selected &&
492
+ this.state.selected &&
493
+ rowIndex + 1 === this.state.selected.row &&
494
+ cellIndex === this.state.selected.cell &&
495
+ this.props.selected
496
+ ? 'selected'
497
+ : ''
498
+ }
499
+ >
500
+ <Cell
501
+ value={cell.value}
502
+ row={rowIndex + 1}
503
+ cell={cellIndex}
504
+ onSelectCell={this.onSelectCell}
505
+ selected={
506
+ this.props.selected &&
507
+ this.state.selected &&
508
+ rowIndex + 1 === this.state.selected.row &&
509
+ cellIndex === this.state.selected.cell
510
+ }
511
+ selectedCell={this.state.selected}
512
+ isTableBlockSelected={this.props.selected}
513
+ onAddBlock={this.props.onAddBlock}
514
+ onSelectBlock={this.props.onSelectBlock}
515
+ onChange={this.onChangeCell}
516
+ index={this.props.index}
517
+ />
518
+ </Table.Cell>
519
+ ))}
520
+ </Table.Row>
521
+ ))}
522
+ </Table.Body>
523
+ </Table>
524
+ )}
525
+ {this.props.selected && this.state.selected && this.state.isClient && (
526
+ <SidebarPortal selected={this.props.selected}>
527
+ <BlockDataForm
528
+ schema={schema}
529
+ title={schema.title}
530
+ onChangeField={(id, value) => {
531
+ this.props.onChangeBlock(this.props.block, {
532
+ ...this.props.data,
533
+ [id]: value,
534
+ });
535
+ }}
536
+ onChangeBlock={this.props.onChangeBlock}
537
+ formData={this.props.data}
538
+ block={this.props.block}
539
+ blocksConfig={this.props.blocksConfig}
540
+ />
541
+ </SidebarPortal>
542
+ )}
543
+ </div>
544
+ );
545
+ }
546
+ }
547
+
548
+ export default injectIntl(Edit);