@eeacms/volto-clms-theme 1.0.118 → 1.0.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/package.json +1 -1
  3. package/src/components/Blocks/CclHomeBgImageBlock/CclGreenBgView.jsx +2 -0
  4. package/src/components/Blocks/CclTextLinkCarouselBlock/CclTextLinkCarouselView.jsx +9 -1
  5. package/src/components/Blocks/CustomTemplates/VoltoSearchBlock/CheckboxTreeParentFacet.jsx +4 -11
  6. package/src/components/Blocks/CustomTemplates/VoltoSearchBlock/FilterList.jsx +38 -3
  7. package/src/components/Blocks/CustomTemplates/VoltoSearchBlock/rewriteOptions.js +14 -0
  8. package/src/components/Blocks/CustomTemplates/VoltoSearchBlock/utils.js +13 -1
  9. package/src/components/Blocks/CustomTemplates/VoltoTabsBlock/custom.less +1 -0
  10. package/src/components/CLMSDatasetDetailView/CLMSDatasetDetailView.jsx +3 -5
  11. package/src/components/CLMSDatasetDetailView/DataSetInfoContent.jsx +4 -15
  12. package/src/components/CLMSDatasetDetailView/DownloadDataSetContent.jsx +7 -1
  13. package/src/components/CLMSDatasetDetailView/RelatedUseCases.jsx +1 -1
  14. package/src/components/CLMSMeetingView/CLMSMeetingView.jsx +7 -1
  15. package/src/components/CclCard/CclCard.jsx +23 -20
  16. package/src/components/CclCard/cards.less +3 -12
  17. package/src/components/CclLoginModal/CclLoginModal.jsx +2 -5
  18. package/src/components/Widgets/TaxonomyWidget.jsx +1 -1
  19. package/src/components/Widgets/taxonomyUtils.js +2 -1
  20. package/src/customizations/volto/components/manage/Widgets/ArrayWidget.jsx +413 -0
  21. package/theme/clms/css/carousel.css +2 -4
  22. package/theme/clms/css/home.css +1 -2
  23. package/theme/clms/css/maps.less +8 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.0.121](https://github.com/eea/volto-clms-theme/compare/1.0.120...1.0.121) - 5 September 2022
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix: CLMS-1224 this file should be removed with volto-16 [Unai - [`7cf72ab`](https://github.com/eea/volto-clms-theme/commit/7cf72aba7b1d69cf1b310a89410aa3a3753f01f3)]
12
+ - fix:lint problems [Unai - [`4e8b9dc`](https://github.com/eea/volto-clms-theme/commit/4e8b9dc6c3cb6813baeb4b75fea5afb1198d9640)]
13
+ - fix: not count children filters on checboxParentFacets [ionlizarazu - [`4b21b1c`](https://github.com/eea/volto-clms-theme/commit/4b21b1c3337e132248ad1ada6a9bf268c5332579)]
14
+ - fix: related use cases [Unai - [`381af05`](https://github.com/eea/volto-clms-theme/commit/381af058fe7484805e5175600b40cd80d52d2c9e)]
15
+ - fix: changed data resource abstract with overview [Unai - [`c630b7e`](https://github.com/eea/volto-clms-theme/commit/c630b7e6a6e093319473554a4a724fd320783f2d)]
16
+ - fix:redirect to download from map viewer when necesary [Unai - [`9ad9d78`](https://github.com/eea/volto-clms-theme/commit/9ad9d78e7e7b946c34da20d7edea2d68c7cd6fe5)]
17
+ - fix: file card modified to show effective date [Unai - [`b2467e7`](https://github.com/eea/volto-clms-theme/commit/b2467e7a3a813871811388e92b308aea62aa688c)]
18
+ - fix:technical documents on datasets general info view [Unai - [`532cf09`](https://github.com/eea/volto-clms-theme/commit/532cf094933c9d0ba1b9fa77609555621645972a)]
19
+ - fix:carousel link open in new tab [Unai - [`bcc39a5`](https://github.com/eea/volto-clms-theme/commit/bcc39a5b921fee81a653743afd1f219a79c4748f)]
20
+
21
+ #### :nail_care: Enhancements
22
+
23
+ - refactor: removed overview title from dataset [Unai - [`e08ff03`](https://github.com/eea/volto-clms-theme/commit/e08ff0348e2a371b033206e3e66f900f09edd443)]
24
+
25
+ #### :hammer_and_wrench: Others
26
+
27
+ - merge sprint-31 branch [Mikel Larreategi - [`617b5d2`](https://github.com/eea/volto-clms-theme/commit/617b5d25a17cdd264b017a8440d3c4c1c9e283bd)]
28
+ - rename variable [ionlizarazu - [`7b4c537`](https://github.com/eea/volto-clms-theme/commit/7b4c53783e4bbe676ee6e7982610b191a9b40532)]
29
+ ### [1.0.120](https://github.com/eea/volto-clms-theme/compare/1.0.119...1.0.120) - 2 September 2022
30
+
31
+ #### :bug: Bug Fixes
32
+
33
+ - fix: use plain a for EU Login URL [Mikel Larreategi - [`e8d2d66`](https://github.com/eea/volto-clms-theme/commit/e8d2d664e750df517a8d955c4833a70e0f8b34e3)]
34
+
35
+ #### :hammer_and_wrench: Others
36
+
37
+ - Map login button fix [rodriama - [`0e7e520`](https://github.com/eea/volto-clms-theme/commit/0e7e520ac2515846b4af6e782a9f4cdc2375a467)]
38
+ - Card doc fix [rodriama - [`556e33c`](https://github.com/eea/volto-clms-theme/commit/556e33c833b9980edab3b778fc408e662fd5fffd)]
39
+ - ESLint fix [rodriama - [`d55db81`](https://github.com/eea/volto-clms-theme/commit/d55db818a02be4ccfcc524c1f03005600644a9e4)]
40
+ - Credits pop up fix [rodriama - [`3772c16`](https://github.com/eea/volto-clms-theme/commit/3772c163a0a998a7dc7b0fddd176346f2d56a4e1)]
41
+ - ESLint fix [rodriama - [`08a9bd8`](https://github.com/eea/volto-clms-theme/commit/08a9bd8c3cc78d928df58cc8b66fa3aea81f2b13)]
42
+ - Event detail segment [rodriama - [`524e3ff`](https://github.com/eea/volto-clms-theme/commit/524e3fff130ae53ffb09aa534d120677e5fd1c1a)]
43
+ ### [1.0.119](https://github.com/eea/volto-clms-theme/compare/1.0.118...1.0.119) - 1 September 2022
44
+
45
+ #### :bug: Bug Fixes
46
+
47
+ - fix: restore taxonomy item ordering [Mikel Larreategi - [`5080a9f`](https://github.com/eea/volto-clms-theme/commit/5080a9f456888bba77042b673f62e3db5cfe86fb)]
48
+ - fix: reverse checkbox label ordering [ionlizarazu - [`cd2ea37`](https://github.com/eea/volto-clms-theme/commit/cd2ea377f9f6c4cdd584dd9c5df41e3574fcd686)]
49
+ - fix: search facet checkbox functionality. Order by title and replace starting text 00# from the checkbox labels [ionlizarazu - [`df69da5`](https://github.com/eea/volto-clms-theme/commit/df69da5124b25ac098b4204e20663524c2ebb91a)]
50
+
7
51
  ### [1.0.118](https://github.com/eea/volto-clms-theme/compare/1.0.117...1.0.118) - 1 September 2022
8
52
 
9
53
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-clms-theme",
3
- "version": "1.0.118",
3
+ "version": "1.0.121",
4
4
  "description": "volto-clms-theme: Volto theme for CLMS site",
5
5
  "main": "src/index.js",
6
6
  "author": "CodeSyntax for the European Environment Agency",
@@ -69,6 +69,8 @@ const CclGreenBgView = (props) => {
69
69
  <a
70
70
  className="ccl-banner-info-link"
71
71
  href={'' + data?.locationHref?.[0]?.['@id']}
72
+ target="_blank"
73
+ rel="noreferrer"
72
74
  >
73
75
  More info
74
76
  </a>
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import loadable from '@loadable/component';
3
+ import { UniversalLink } from '@plone/volto/components';
3
4
  const Slider = loadable(() => import('react-slick'));
4
5
 
5
6
  const CclTextLinkCarouselView = (props) => {
@@ -24,7 +25,14 @@ const CclTextLinkCarouselView = (props) => {
24
25
  {data?.textLink?.items.map((item, index) => (
25
26
  <div className="text-link-carousel-block" key={index}>
26
27
  <div className="text-link-carousel-block-content">
27
- <a href={'' + item?.link?.[0]?.['@id']}>{item?.text}</a>
28
+ <UniversalLink
29
+ openLinkInNewTab={true}
30
+ href={'' + item?.link?.[0]?.['@id']}
31
+ >
32
+ {item?.text}
33
+ </UniversalLink>
34
+
35
+ {/* <a href={'' + item?.link?.[0]?.['@id']}>{item?.text}</a> */}
28
36
  </div>
29
37
  </div>
30
38
  ))}
@@ -10,7 +10,7 @@ import {
10
10
  selectFacetStateToValue,
11
11
  selectFacetValueToQuery,
12
12
  } from '@plone/volto/components/manage/Blocks/Search/components/base';
13
- import { checkAllChildren } from './utils';
13
+ import { checkAllChildren, uncheckOptionAndChildren } from './utils';
14
14
 
15
15
  const hasAllChildrensSelected = (value, childrens) => {
16
16
  var result = true;
@@ -92,16 +92,9 @@ const CheckboxListParent = ({ option, key, onChange, value, id }) => {
92
92
  ...checkAllChildren(value, option).map((f) => f.value),
93
93
  ])
94
94
  : onChange(id, [
95
- ...value
96
- .filter((item) => item.value !== option.value)
97
- .filter(
98
- (item) =>
99
- option.childrens?.length > 0 &&
100
- !option.childrens
101
- .map((ch) => ch.value)
102
- .includes(item.value),
103
- )
104
- .map((f) => f.value),
95
+ ...uncheckOptionAndChildren(value, option).map(
96
+ (f) => f.value,
97
+ ),
105
98
  ]);
106
99
  }}
107
100
  label={
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Button, Icon } from 'semantic-ui-react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
+ import { structure_taxonomy_terms } from '@eeacms/volto-clms-theme/components';
4
5
 
5
6
  const messages = defineMessages({
6
7
  currentFilters: {
@@ -14,7 +15,7 @@ const messages = defineMessages({
14
15
  });
15
16
 
16
17
  const FilterList = (props) => {
17
- const { facets, setFacets, isEditMode, data } = props;
18
+ const { facets, setFacets, isEditMode, data, querystring } = props;
18
19
  const showFilterList = !Object.values(facets).every((facet) => !facet.length);
19
20
 
20
21
  const baseFacets = data.facets;
@@ -28,10 +29,44 @@ const FilterList = (props) => {
28
29
  baseFacets.map((bf) => bf.field?.value).includes(v[0]),
29
30
  ),
30
31
  );
32
+ const fieldsToAvoidChildren = data.facets
33
+ .filter((item) => item.type === 'checkboxTreeParentFacet')
34
+ .map((item) => item.field.value);
35
+ let filtersToAvoid = [];
36
+ if (querystring.loaded) {
37
+ filtersToAvoid = fieldsToAvoidChildren
38
+ .map((field) => {
39
+ let result = [];
40
+ const fieldValuesDict = querystring.indexes[field].values;
41
+ const fieldValues = Object.keys(fieldValuesDict).map((fieldKey) => {
42
+ return { value: fieldKey, label: fieldValuesDict[fieldKey].title };
43
+ });
44
+ const fieldStructuredValues = structure_taxonomy_terms(fieldValues);
45
+ fieldStructuredValues.forEach((parent) => {
46
+ parent.childrens.forEach((children) => result.push(children.value));
47
+ });
48
+ return result;
49
+ })
50
+ .flat(1);
51
+ }
52
+ const filtersToAvoidSet = new Set(filtersToAvoid);
31
53
 
32
- const totalFilters = [].concat.apply([], Object.values(currentFilters))
33
- .length;
54
+ // if (choices?.length > 0) {
55
+ // options = structure_taxonomy_terms(choices);
56
+ // }
57
+ const currentFiltersToCount = {};
58
+ Object.keys(currentFilters).forEach((filterKey) => {
59
+ currentFiltersToCount[filterKey] = currentFilters[filterKey].filter(
60
+ (filter) => {
61
+ return !filtersToAvoidSet.has(filter);
62
+ },
63
+ );
64
+ });
65
+ // const totalFilters = [].concat.apply([], Object.values(currentFilters))
66
+ // .length;
34
67
 
68
+ const totalFilters = [].concat.apply([], Object.values(currentFiltersToCount))
69
+ .length;
35
70
  const intl = useIntl();
36
71
 
37
72
  return showFilterList && Object.keys(currentFilters).length ? (
@@ -80,6 +80,20 @@ const rewriteOptions = (name, choices) => {
80
80
  return 0;
81
81
  });
82
82
  }
83
+ if (name === 'taxonomy_technical_library_categorization') {
84
+ result = choices
85
+ .sort((a, b) => {
86
+ if (a.label < b.label) {
87
+ return -1;
88
+ } else if (a.label > b.label) {
89
+ return 1;
90
+ }
91
+ return 0;
92
+ })
93
+ .map((opt) => {
94
+ return { ...opt, label: opt.label.replace(/^[0-9][0-9]#/, '') };
95
+ });
96
+ }
83
97
  return result;
84
98
  };
85
99
 
@@ -7,5 +7,17 @@ export const checkAllChildren = (value, option) => {
7
7
  value.push(ch);
8
8
  }
9
9
  });
10
- return value;
10
+ return [...value, { label: option.label, value: option.value }];
11
+ };
12
+
13
+ export const uncheckOptionAndChildren = (value, option) => {
14
+ return value
15
+ .filter((item) => item.value !== option.value)
16
+ .filter((item) => {
17
+ if (option.childrens?.length > 0) {
18
+ return !option.childrens.map((ch) => ch.value).includes(item.value);
19
+ } else {
20
+ return true;
21
+ }
22
+ });
11
23
  };
@@ -32,6 +32,7 @@ div.tabs-block.CCLCarousel {
32
32
  .text-link-carousel-block {
33
33
  height: 2rem;
34
34
  padding: 0;
35
+ margin-left: 0.5rem;
35
36
  }
36
37
 
37
38
  .text-link-carousel-block-content {
@@ -77,6 +77,7 @@ const CLMSDatasetDetailView = ({ content, token }) => {
77
77
  key={item.id}
78
78
  loading={geonetwork_importation.loading}
79
79
  circular
80
+ style={{ width: '50%' }}
80
81
  >
81
82
  <strong>
82
83
  {item.title} (from {item.type}):{' '}
@@ -203,6 +204,7 @@ const CLMSDatasetDetailView = ({ content, token }) => {
203
204
  key={'wms-layers-import'}
204
205
  loading={wms_layers_importation?.loading}
205
206
  circular
207
+ style={{ width: '50%' }}
206
208
  >
207
209
  <Modal
208
210
  onClose={() => {
@@ -288,17 +290,13 @@ const CLMSDatasetDetailView = ({ content, token }) => {
288
290
  </p>
289
291
  )}
290
292
  </Segment>
291
- </Segment.Group>
292
- )}
293
-
294
- {user?.roles && user.roles.includes('Manager') && (
295
- <Segment.Group compact horizontal>
296
293
  <Segment
297
294
  padded={'very'}
298
295
  color={'olive'}
299
296
  key={'wms-fields-import'}
300
297
  loading={wms_fields_importation?.loading}
301
298
  circular
299
+ style={{ width: '50%' }}
302
300
  >
303
301
  <Modal
304
302
  onClose={() => {
@@ -14,22 +14,14 @@ import { useLocation } from 'react-router-dom';
14
14
 
15
15
  const DataSetInfoContent = (props) => {
16
16
  const dispatch = useDispatch();
17
- const {
18
- UID,
19
- id,
20
- validation,
21
- dataResourceAbstract,
22
- data,
23
- geonetwork_identifiers,
24
- citation,
25
- } = props;
17
+ const { UID, id, validation, data, geonetwork_identifiers, citation } = props;
26
18
  const location = useLocation();
27
19
  const searchSubrequests = useSelector((state) => state.search.subrequests);
28
20
  let libraries = searchSubrequests?.[id]?.items || [];
29
21
  let librariesPending = searchSubrequests?.[id]?.loading;
30
22
  const user = useSelector((state) => state.users.user);
31
23
  React.useEffect(() => {
32
- if (location.hash === '#GeneralInfo' && UID) {
24
+ if (location.hash === '#General-Info' && UID) {
33
25
  dispatch(
34
26
  searchContent(
35
27
  '',
@@ -66,7 +58,6 @@ const DataSetInfoContent = (props) => {
66
58
  ? iTitleIcons.rightPosition
67
59
  : iTitleIcons.leftPosition;
68
60
  }
69
-
70
61
  return (
71
62
  <div>
72
63
  {validation?.data && validation?.data !== '<p><br/></p>' && (
@@ -77,11 +68,9 @@ const DataSetInfoContent = (props) => {
77
68
  ></CclCitation>
78
69
  )}
79
70
  <CclInfoContainer>
80
- {dataResourceAbstract?.data && (
71
+ {props?.description && (
81
72
  <CclInfoDescription
82
- title="Data resource abstract"
83
- description={<StringToHTML string={dataResourceAbstract.data} />}
84
- tooltip="Brief narrative summary of the content of the resource(s) with coverage, main attributes, data sources, important of the work, etc."
73
+ description={<StringToHTML string={props.description} />}
85
74
  ></CclInfoDescription>
86
75
  )}
87
76
  </CclInfoContainer>
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import CclDownloadTable from '@eeacms/volto-clms-theme/components/CclDownloadTable/CclDownloadTable';
3
3
  import CclButton from '@eeacms/volto-clms-theme/components/CclButton/CclButton';
4
- import { Link, useLocation } from 'react-router-dom';
4
+ import { Link, useLocation, Redirect } from 'react-router-dom';
5
5
 
6
6
  const DownloadDataSetContent = (data) => {
7
7
  let url = '/register';
@@ -9,6 +9,12 @@ const DownloadDataSetContent = (data) => {
9
9
 
10
10
  return (
11
11
  <div>
12
+ {data.downloadable_files?.items[0].path === '' &&
13
+ location.hash === '#Download' ? (
14
+ <Redirect to={location.pathname + '/download-by-area'} />
15
+ ) : (
16
+ ''
17
+ )}
12
18
  {data.token === '' && (
13
19
  <div className="login-block">
14
20
  <div className="login-content">
@@ -14,7 +14,7 @@ const RelatedUseCases = (props) => {
14
14
  let librariesPending = searchSubrequests?.[id]?.loading;
15
15
  const location = useLocation();
16
16
  React.useEffect(() => {
17
- if (location.hash === '#Usecases' && UID) {
17
+ if (location.hash === '#Use-cases' && UID) {
18
18
  dispatch(
19
19
  searchContent(
20
20
  '',
@@ -288,7 +288,13 @@ export const CLMSMeetingView = (props) => {
288
288
  </Segment.Group>
289
289
  )}
290
290
  {content.description}
291
- <Segment compact padded={'very'} color={'olive'} floated="left">
291
+ <Segment
292
+ compact
293
+ padded={'small'}
294
+ color={'olive'}
295
+ floated="right"
296
+ style={{ marginRight: 0 }}
297
+ >
292
298
  <div className="dataset-info-field">
293
299
  <div className="dataset-field-title">
294
300
  <Header>{intl.formatMessage(messages.when)}</Header>
@@ -49,30 +49,30 @@ const CardLink = ({ url, children, className, condition = true }) => {
49
49
  const DocCard = ({ card, url, showEditor, children }) => {
50
50
  return (
51
51
  <>
52
- <div className="card-doc-title">
53
- {card?.Type === 'TechnicalLibrary' ? (
54
- <a href={`${card['@id']}/@@download/file`}>{card?.title}</a>
55
- ) : (
56
- <Link to={url}>{card?.title}</Link>
57
- )}
58
- {card?.Type === 'TechnicalLibrary' && showEditor && (
59
- <Link to={`${url}/edit`}>
60
- <Icon
61
- name={penSVG}
62
- size="15px"
63
- className="circled"
64
- title={'Edit'}
65
- />
66
- </Link>
67
- )}
68
- </div>
69
- <div className="card-doc-text">
70
- <div className="doc-description">{card?.description}</div>
52
+ <div className="card-doc-header">
53
+ <div className="card-doc-title">
54
+ {card?.Type === 'TechnicalLibrary' ? (
55
+ <a href={`${card['@id']}/@@download/file`}>{card?.title}</a>
56
+ ) : (
57
+ <Link to={url}>{card?.title}</Link>
58
+ )}
59
+ {card?.Type === 'TechnicalLibrary' && showEditor && (
60
+ <Link to={`${url}/edit`}>
61
+ <Icon
62
+ name={penSVG}
63
+ size="15px"
64
+ className="circled"
65
+ title={'Edit'}
66
+ />
67
+ </Link>
68
+ )}
69
+ </div>
71
70
  {card?.Type === 'TechnicalLibrary' && (
72
71
  <div className="card-doc-size">{card.getObjSize || ''}</div>
73
72
  )}
74
- {children}
75
73
  </div>
74
+ <div className="card-doc-description">{card?.description}</div>
75
+ {children}
76
76
  </>
77
77
  );
78
78
  };
@@ -140,6 +140,9 @@ function CclCard(props) {
140
140
  </div>
141
141
  <div className="card-text">
142
142
  <CardLink url={url}>{card?.title}</CardLink>
143
+ <div className="news-detail-date">
144
+ {cclDateFormat(card?.effective)}
145
+ </div>
143
146
  <div className="card-description">{card?.description}</div>
144
147
  {children}
145
148
  </div>
@@ -375,13 +375,9 @@
375
375
  border-bottom: solid 1px #a0b12833;
376
376
  }
377
377
 
378
- /* Documents */
379
- .card-doc {
380
- padding: 1rem;
381
- }
382
-
383
- .card-doc:not(:last-of-type) {
384
- border-bottom: solid 1px #a0b12833;
378
+ .card-doc-header {
379
+ display: flex;
380
+ justify-content: space-between;
385
381
  }
386
382
 
387
383
  .card-doc-title {
@@ -391,11 +387,6 @@
391
387
  font-weight: bold;
392
388
  }
393
389
 
394
- .card-doc-text {
395
- display: flex;
396
- justify-content: space-between;
397
- }
398
-
399
390
  .card-doc-size {
400
391
  margin-left: 1rem;
401
392
  white-space: nowrap;
@@ -90,12 +90,9 @@ function CclLoginModal(props) {
90
90
  </div>
91
91
  <div className="actions">
92
92
  <div className="modal-buttons">
93
- <UniversalLink
94
- href={loginUrl || '#'}
95
- className="ccl-button ccl-button-green"
96
- >
93
+ <a href={loginUrl || '#'} className="ccl-button ccl-button-green">
97
94
  Login using EU Login
98
- </UniversalLink>
95
+ </a>
99
96
  </div>
100
97
  </div>
101
98
  </CclModal>
@@ -180,7 +180,7 @@ const CheckboxListParent = ({ option, key, onChange, value, id }) => {
180
180
  }}
181
181
  label={
182
182
  <label htmlFor={`field-${option.value}`}>
183
- {option.label}
183
+ {option.label.replace(/^[0-9][0-9]#/, '')}
184
184
  </label>
185
185
  }
186
186
  checked={
@@ -46,5 +46,6 @@ export const structure_taxonomy_terms = (choices) => {
46
46
  // });
47
47
 
48
48
  // return sort_array_items_by_key(options, 'label');
49
- return options.reverse();
49
+ //return options.reverse();
50
+ return options;
50
51
  };
@@ -0,0 +1,413 @@
1
+ /**
2
+ * ArrayWidget component.
3
+ * @module components/manage/Widgets/ArrayWidget
4
+ */
5
+
6
+ import React, { Component } from 'react';
7
+ import { defineMessages, injectIntl } from 'react-intl';
8
+ import PropTypes from 'prop-types';
9
+ import { compose } from 'redux';
10
+ import { connect } from 'react-redux';
11
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
12
+ import { find, isObject } from 'lodash';
13
+
14
+ import {
15
+ getVocabFromHint,
16
+ getVocabFromField,
17
+ getVocabFromItems,
18
+ } from '@plone/volto/helpers';
19
+ import { getVocabulary } from '@plone/volto/actions';
20
+
21
+ import {
22
+ Option,
23
+ DropdownIndicator,
24
+ ClearIndicator,
25
+ selectTheme,
26
+ customSelectStyles,
27
+ MenuList,
28
+ SortableMultiValue,
29
+ SortableMultiValueLabel,
30
+ } from '@plone/volto/components/manage/Widgets/SelectStyling';
31
+
32
+ import { FormFieldWrapper } from '@plone/volto/components';
33
+
34
+ const messages = defineMessages({
35
+ select: {
36
+ id: 'Select…',
37
+ defaultMessage: 'Select…',
38
+ },
39
+ no_value: {
40
+ id: 'No value',
41
+ defaultMessage: 'No value',
42
+ },
43
+ no_options: {
44
+ id: 'No options',
45
+ defaultMessage: 'No options',
46
+ },
47
+ });
48
+
49
+ function arrayMove(array, from, to) {
50
+ const slicedArray = array.slice();
51
+ slicedArray.splice(
52
+ to < 0 ? array.length + to : to,
53
+ 0,
54
+ slicedArray.splice(from, 1)[0],
55
+ );
56
+ return slicedArray;
57
+ }
58
+
59
+ function normalizeArrayValue(choices, value) {
60
+ if (!value || !Array.isArray(value)) return [];
61
+ if (value.length === 0) return value;
62
+
63
+ if (typeof value[0] === 'string') {
64
+ // raw value like ['foo', 'bar']
65
+ return value.map((v) => {
66
+ return {
67
+ label: find(choices, (c) => c.value === v)?.label || v,
68
+ value: v,
69
+ };
70
+ });
71
+ }
72
+
73
+ if (
74
+ isObject(value[0]) &&
75
+ Object.keys(value[0]).includes('token') // Array of objects, w/ label+value
76
+ ) {
77
+ return value
78
+ .map((v) => {
79
+ const item = find(choices, (c) => c.value === v.token);
80
+ return item
81
+ ? {
82
+ label: item.label || item.title || item.token,
83
+ value: v.token,
84
+ }
85
+ : {
86
+ // avoid a crash if choices doesn't include this item
87
+ label: v.label,
88
+ value: v.token,
89
+ };
90
+ })
91
+ .filter((f) => !!f);
92
+ }
93
+
94
+ return [];
95
+ }
96
+
97
+ function normalizeChoices(choices) {
98
+ if (Array.isArray(choices) && choices.length && Array.isArray(choices[0])) {
99
+ return choices.map((option) => ({
100
+ value: option[0],
101
+ label:
102
+ // Fix "None" on the serializer, to remove when fixed in p.restapi
103
+ option[1] !== 'None' && option[1] ? option[1] : option[0],
104
+ }));
105
+ }
106
+
107
+ return choices;
108
+ }
109
+
110
+ /**
111
+ * Compare values and return true if equal.
112
+ * Consider upper and lower case.
113
+ * @method compareOption
114
+ * @param {*} inputValue
115
+ * @param {*} option
116
+ * @param {*} accessors
117
+ * @returns {boolean}
118
+ */
119
+ const compareOption = (inputValue = '', option, accessors) => {
120
+ const candidate = String(inputValue);
121
+ const optionValue = String(accessors.getOptionValue(option));
122
+ const optionLabel = String(accessors.getOptionLabel(option));
123
+ return optionValue === candidate || optionLabel === candidate;
124
+ };
125
+
126
+ /**
127
+ * ArrayWidget component class.
128
+ * @class ArrayWidget
129
+ * @extends Component
130
+ *
131
+ * A createable select array widget will be rendered if the named vocabulary is
132
+ * in the widget definition (hint) like:
133
+ *
134
+ * ```
135
+ * list_field_voc_unconstrained = schema.List(
136
+ * title=u"List field with values from vocabulary but not constrained to them.",
137
+ * description=u"zope.schema.List",
138
+ * value_type=schema.TextLine(),
139
+ * required=False,
140
+ * missing_value=[],
141
+ * )
142
+ * directives.widget(
143
+ * "list_field_voc_unconstrained",
144
+ * AjaxSelectFieldWidget,
145
+ * vocabulary="plone.app.vocabularies.PortalTypes",
146
+ * )
147
+ * ```
148
+ */
149
+ class ArrayWidget extends Component {
150
+ /**
151
+ * Property types.
152
+ * @property {Object} propTypes Property types.
153
+ * @static
154
+ */
155
+ static propTypes = {
156
+ id: PropTypes.string.isRequired,
157
+ title: PropTypes.string.isRequired,
158
+ description: PropTypes.string,
159
+ required: PropTypes.bool,
160
+ error: PropTypes.arrayOf(PropTypes.string),
161
+ getVocabulary: PropTypes.func.isRequired,
162
+ choices: PropTypes.arrayOf(
163
+ PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
164
+ ),
165
+ vocabLoading: PropTypes.bool,
166
+ vocabLoaded: PropTypes.bool,
167
+ items: PropTypes.shape({
168
+ vocabulary: PropTypes.object,
169
+ }),
170
+ widgetOptions: PropTypes.shape({
171
+ vocabulary: PropTypes.object,
172
+ }),
173
+ value: PropTypes.arrayOf(
174
+ PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
175
+ ),
176
+ onChange: PropTypes.func.isRequired,
177
+ wrapped: PropTypes.bool,
178
+ creatable: PropTypes.bool, //if widget has no vocab and you want to be creatable
179
+ };
180
+
181
+ /**
182
+ * Default properties
183
+ * @property {Object} defaultProps Default properties.
184
+ * @static
185
+ */
186
+ static defaultProps = {
187
+ description: null,
188
+ required: false,
189
+ items: {
190
+ vocabulary: null,
191
+ },
192
+ widgetOptions: {
193
+ vocabulary: null,
194
+ },
195
+ error: [],
196
+ choices: [],
197
+ value: null,
198
+ creatable: false,
199
+ };
200
+
201
+ /**
202
+ * Constructor
203
+ * @method constructor
204
+ * @param {Object} props Component properties
205
+ * @constructs Actions
206
+ */
207
+ constructor(props) {
208
+ super(props);
209
+
210
+ this.handleChange = this.handleChange.bind(this);
211
+ }
212
+
213
+ /**
214
+ * Component did mount
215
+ * @method componentDidMount
216
+ * @returns {undefined}
217
+ */
218
+ componentDidMount() {
219
+ if (
220
+ !this.props.items?.choices?.length &&
221
+ !this.props.choices?.length &&
222
+ this.props.vocabBaseUrl
223
+ ) {
224
+ this.props.getVocabulary({
225
+ vocabNameOrURL: this.props.vocabBaseUrl,
226
+ size: -1,
227
+ subrequest: this.props.lang,
228
+ });
229
+ }
230
+ }
231
+
232
+ componentDidUpdate() {
233
+ if (
234
+ !this.props.items?.choices?.length &&
235
+ !this.props.choices?.length &&
236
+ this.props.vocabLoading === undefined &&
237
+ !this.props.vocabLoaded
238
+ ) {
239
+ this.props.getVocabulary({
240
+ vocabNameOrURL: this.props.vocabBaseUrl,
241
+ size: -1,
242
+ subrequest: this.props.lang,
243
+ });
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Handle the field change, store it in the local state and back to simple
249
+ * array of tokens for correct serialization
250
+ * @method handleChange
251
+ * @param {array} selectedOption The selected options (already aggregated).
252
+ * @returns {undefined}
253
+ */
254
+ handleChange(selectedOption) {
255
+ this.props.onChange(
256
+ this.props.id,
257
+ selectedOption ? selectedOption.map((item) => item.value) : null,
258
+ );
259
+ }
260
+
261
+ onSortEnd = (selectedOption, { oldIndex, newIndex }) => {
262
+ const newValue = arrayMove(selectedOption, oldIndex, newIndex);
263
+
264
+ this.handleChange(newValue);
265
+ };
266
+
267
+ /**
268
+ * Render method.
269
+ * @method render
270
+ * @returns {string} Markup for the component.
271
+ */
272
+ render() {
273
+ const choices = normalizeChoices(this.props?.choices || []);
274
+ const selectedOption = normalizeArrayValue(choices, this.props.value);
275
+
276
+ const CreatableSelect = this.props.reactSelectCreateable.default;
277
+ const { SortableContainer } = this.props.reactSortableHOC;
278
+ const Select = this.props.reactSelect.default;
279
+ const SortableSelect =
280
+ // It will be only createable if the named vocabulary is in the widget definition
281
+ // (hint) like:
282
+ // list_field_voc_unconstrained = schema.List(
283
+ // title=u"List field with values from vocabulary but not constrained to them.",
284
+ // description=u"zope.schema.List",
285
+ // value_type=schema.TextLine(),
286
+ // required=False,
287
+ // missing_value=[],
288
+ // )
289
+ // directives.widget(
290
+ // "list_field_voc_unconstrained",
291
+ // AjaxSelectFieldWidget,
292
+ // vocabulary="plone.app.vocabularies.PortalTypes",
293
+ // )
294
+ this.props?.choices &&
295
+ !getVocabFromHint(this.props) &&
296
+ !this.props.creatable
297
+ ? SortableContainer(Select)
298
+ : SortableContainer(CreatableSelect);
299
+
300
+ return (
301
+ <FormFieldWrapper {...this.props}>
302
+ <SortableSelect
303
+ useDragHandle
304
+ // react-sortable-hoc props:
305
+ axis="xy"
306
+ onSortEnd={this.onSortEnd}
307
+ distance={4}
308
+ // small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
309
+ getHelperDimensions={({ node }) => node.getBoundingClientRect()}
310
+ id={`field-${this.props.id}`}
311
+ key={this.props.id}
312
+ isDisabled={this.props.disabled || this.props.isDisabled}
313
+ className="react-select-container"
314
+ classNamePrefix="react-select"
315
+ options={
316
+ this.props.vocabBaseUrl
317
+ ? choices
318
+ : this.props.choices
319
+ ? [
320
+ ...choices,
321
+ ...(this.props.noValueOption && !this.props.default
322
+ ? [
323
+ {
324
+ label: this.props.intl.formatMessage(
325
+ messages.no_value,
326
+ ),
327
+ value: 'no-value',
328
+ },
329
+ ]
330
+ : []),
331
+ ]
332
+ : [
333
+ {
334
+ label: this.props.intl.formatMessage(messages.no_value),
335
+ value: 'no-value',
336
+ },
337
+ ]
338
+ }
339
+ styles={customSelectStyles}
340
+ theme={selectTheme}
341
+ components={{
342
+ ...(this.props.choices?.length > 25 && {
343
+ MenuList,
344
+ }),
345
+ MultiValue: SortableMultiValue,
346
+ MultiValueLabel: SortableMultiValueLabel,
347
+ DropdownIndicator,
348
+ ClearIndicator,
349
+ Option,
350
+ }}
351
+ value={selectedOption || []}
352
+ placeholder={this.props.intl.formatMessage(messages.select)}
353
+ onChange={this.handleChange}
354
+ isValidNewOption={(
355
+ inputValue,
356
+ selectValue,
357
+ selectOptions,
358
+ accessors,
359
+ ) =>
360
+ !(
361
+ !inputValue ||
362
+ selectValue.some((option) =>
363
+ compareOption(inputValue, option, accessors),
364
+ ) ||
365
+ selectOptions.some((option) =>
366
+ compareOption(inputValue, option, accessors),
367
+ )
368
+ )
369
+ }
370
+ isClearable
371
+ isMulti
372
+ />
373
+ </FormFieldWrapper>
374
+ );
375
+ }
376
+ }
377
+
378
+ export const ArrayWidgetComponent = injectIntl(ArrayWidget);
379
+
380
+ export default compose(
381
+ injectIntl,
382
+ injectLazyLibs(['reactSelect', 'reactSelectCreateable', 'reactSortableHOC']),
383
+ connect(
384
+ (state, props) => {
385
+ const vocabBaseUrl =
386
+ getVocabFromHint(props) ||
387
+ getVocabFromField(props) ||
388
+ getVocabFromItems(props);
389
+
390
+ const vocabState =
391
+ state.vocabularies?.[vocabBaseUrl]?.subrequests?.[state.intl.locale];
392
+
393
+ // If the schema already has the choices in it, then do not try to get the vocab,
394
+ // even if there is one
395
+ if (props.items?.choices) {
396
+ return {
397
+ choices: props.items.choices,
398
+ lang: state.intl.locale,
399
+ };
400
+ } else if (vocabState) {
401
+ return {
402
+ choices: vocabState.items,
403
+ vocabBaseUrl,
404
+ vocabLoading: vocabState.loading,
405
+ vocabLoaded: vocabState.loaded,
406
+ lang: state.intl.locale,
407
+ };
408
+ }
409
+ return { vocabBaseUrl, lang: state.intl.locale };
410
+ },
411
+ { getVocabulary },
412
+ ),
413
+ )(ArrayWidget);
@@ -496,7 +496,7 @@
496
496
  .ccl-banner-info {
497
497
  position: absolute;
498
498
  z-index: 1;
499
- right: 0;
499
+ right: 2rem;
500
500
  bottom: 2rem;
501
501
  display: none;
502
502
  width: 100%;
@@ -524,9 +524,7 @@
524
524
  }
525
525
 
526
526
  .ccl-banner-info-link {
527
- display: inline-block;
528
- width: 100%;
529
- text-align: right;
527
+ float: right;
530
528
  }
531
529
 
532
530
  .tabs-block > .styled {
@@ -68,7 +68,6 @@
68
68
 
69
69
  .ccl-banner-top-bar {
70
70
  height: 2rem;
71
- padding: 0 1rem;
72
71
  border-top: 1px solid #6c6e73;
73
72
  border-bottom: 1px solid #6c6e73;
74
73
  background-color: #a0b128;
@@ -91,13 +90,13 @@
91
90
  }
92
91
 
93
92
  .ccl-banner-top-bar .ccl-container .ccl-banner-top-bar-right {
94
- margin-left: 0.5rem;
95
93
  cursor: pointer;
96
94
  }
97
95
 
98
96
  .ccl-banner-top-bar .ccl-container {
99
97
  display: flex;
100
98
  height: 100%;
99
+ padding: 0 2rem;
101
100
  align-items: center;
102
101
  justify-content: space-between;
103
102
  }
@@ -583,7 +583,14 @@
583
583
  margin: 0;
584
584
  background: none;
585
585
  float: none;
586
- font-size: 0.85rem;
586
+ }
587
+
588
+ .login-panel .login-text {
589
+ margin-bottom: 1rem;
590
+ }
591
+
592
+ .login-panel .login-block .ccl-button {
593
+ margin: 0;
587
594
  }
588
595
 
589
596
  #login_close {