@plone/volto 17.0.0-alpha.25 → 17.0.0-alpha.26

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 (95) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +52 -5
  3. package/README.md +8 -7
  4. package/cypress/support/commands.js +12 -9
  5. package/cypress.config.js +1 -0
  6. package/locales/ca/LC_MESSAGES/volto.po +36 -15
  7. package/locales/ca.json +1 -1
  8. package/locales/de/LC_MESSAGES/volto.po +36 -15
  9. package/locales/de.json +1 -1
  10. package/locales/en/LC_MESSAGES/volto.po +35 -14
  11. package/locales/en.json +1 -1
  12. package/locales/es/LC_MESSAGES/volto.po +65 -44
  13. package/locales/es.json +1 -1
  14. package/locales/eu/LC_MESSAGES/volto.po +35 -14
  15. package/locales/eu.json +1 -1
  16. package/locales/fi/LC_MESSAGES/volto.po +35 -14
  17. package/locales/fi.json +1 -1
  18. package/locales/fr/LC_MESSAGES/volto.po +36 -15
  19. package/locales/fr.json +1 -1
  20. package/locales/it/LC_MESSAGES/volto.po +35 -14
  21. package/locales/it.json +1 -1
  22. package/locales/ja/LC_MESSAGES/volto.po +35 -14
  23. package/locales/ja.json +1 -1
  24. package/locales/nl/LC_MESSAGES/volto.po +36 -15
  25. package/locales/nl.json +1 -1
  26. package/locales/pt/LC_MESSAGES/volto.po +36 -15
  27. package/locales/pt.json +1 -1
  28. package/locales/pt_BR/LC_MESSAGES/volto.po +35 -14
  29. package/locales/pt_BR.json +1 -1
  30. package/locales/ro/LC_MESSAGES/volto.po +36 -15
  31. package/locales/ro.json +1 -1
  32. package/locales/volto.pot +35 -14
  33. package/locales/zh_CN/LC_MESSAGES/volto.po +36 -15
  34. package/locales/zh_CN.json +1 -1
  35. package/package.json +4 -4
  36. package/packages/volto-slate/package.json +1 -1
  37. package/packages/volto-slate/src/editor/render.jsx +2 -3
  38. package/src/actions/index.js +3 -0
  39. package/src/actions/navroot/navroot.js +16 -0
  40. package/src/actions/navroot/navroot.test.js +15 -0
  41. package/src/actions/site/site.js +16 -0
  42. package/src/actions/site/site.test.js +15 -0
  43. package/src/actions/userSession/userSession.js +17 -1
  44. package/src/components/manage/Blocks/Block/Settings.jsx +2 -0
  45. package/src/components/manage/Blocks/Block/Settings.test.jsx +90 -0
  46. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +1 -1
  47. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +42 -25
  48. package/src/components/manage/Blocks/ToC/View.jsx +75 -13
  49. package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +2 -12
  50. package/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +65 -38
  51. package/src/components/manage/Controlpanels/Rules/AddRule.jsx +1 -1
  52. package/src/components/manage/Controlpanels/Rules/EditRule.jsx +1 -1
  53. package/src/components/manage/Controlpanels/Users/RenderUsers.jsx +95 -5
  54. package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +116 -103
  55. package/src/components/manage/Form/BlockDataForm.jsx +3 -2
  56. package/src/components/manage/Form/BlockDataForm.test.jsx +34 -2
  57. package/src/components/manage/LinksToItem/LinksToItem.test.jsx +4 -1
  58. package/src/components/manage/Messages/Messages.jsx +32 -99
  59. package/src/components/manage/Messages/Messages.test.jsx +0 -1
  60. package/src/components/manage/Sharing/Sharing.jsx +39 -16
  61. package/src/components/manage/UniversalLink/UniversalLink.jsx +4 -6
  62. package/src/components/manage/Widgets/ArrayWidget.jsx +3 -1
  63. package/src/components/manage/Widgets/ArrayWidget.test.jsx +45 -1
  64. package/src/components/manage/Widgets/RegistryImageWidget.jsx +210 -0
  65. package/src/components/manage/Widgets/RegistryImageWidget.test.jsx +91 -0
  66. package/src/components/manage/Widgets/SelectWidget.jsx +15 -1
  67. package/src/components/manage/Widgets/SelectWidget.test.jsx +45 -1
  68. package/src/components/theme/ContentMetadataTags/ContentMetadataTags.jsx +37 -3
  69. package/src/components/theme/Login/Login.jsx +159 -241
  70. package/src/components/theme/Logo/Logo.Multilingual.test.jsx +131 -1
  71. package/src/components/theme/Logo/Logo.jsx +35 -29
  72. package/src/components/theme/Logo/Logo.test.jsx +135 -1
  73. package/src/components/theme/Logout/Logout.jsx +1 -1
  74. package/src/components/theme/Navigation/Navigation.jsx +86 -171
  75. package/src/components/theme/SearchWidget/SearchWidget.jsx +15 -3
  76. package/src/components/theme/SearchWidget/SearchWidget.test.jsx +8 -0
  77. package/src/components/theme/View/View.jsx +2 -0
  78. package/src/config/ControlPanels.js +0 -1
  79. package/src/config/Widgets.jsx +2 -0
  80. package/src/config/index.js +15 -3
  81. package/src/constants/ActionTypes.js +3 -0
  82. package/src/express-middleware/images.js +1 -0
  83. package/src/helpers/MessageLabels/MessageLabels.js +26 -4
  84. package/src/helpers/Site/index.js +21 -0
  85. package/src/helpers/index.js +1 -0
  86. package/src/reducers/index.js +4 -0
  87. package/src/reducers/navroot/navroot.js +79 -0
  88. package/src/reducers/navroot/navroot.test.js +110 -0
  89. package/src/reducers/site/site.js +51 -0
  90. package/src/reducers/site/site.test.js +67 -0
  91. package/src/reducers/userSession/userSession.js +15 -1
  92. package/test-setup-config.js +1 -0
  93. package/theme/themes/pastanaga/elements/input.overrides +5 -1
  94. package/theme/themes/pastanaga/extras/login.less +3 -0
  95. package/webpack-plugins/webpack-less-plugin.js +19 -0
@@ -5,6 +5,8 @@ import { useLocation, useHistory } from 'react-router-dom';
5
5
 
6
6
  import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
7
7
  import config from '@plone/volto/registry';
8
+ import { usePrevious } from '@plone/volto/helpers';
9
+ import { isEqual } from 'lodash';
8
10
 
9
11
  function getDisplayName(WrappedComponent) {
10
12
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
@@ -253,35 +255,50 @@ const withSearch = (options) => (WrappedComponent) => {
253
255
  const multiFacets = data.facets
254
256
  ?.filter((facet) => facet?.multiple)
255
257
  .map((facet) => facet?.field?.value);
256
- const [facets, setFacets] = React.useState(
257
- Object.assign(
258
- {},
259
- ...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
260
-
261
- // support for simple filters like ?Subject=something
262
- // TODO: since the move to hash params this is no longer working.
263
- // We'd have to treat the location.search and manage it just like the
264
- // hash, to support it. We can read it, but we'd have to reset it as
265
- // well, so at that point what's the difference to the hash?
266
- ...configuredFacets.map((f) =>
267
- locationSearchData[f]
268
- ? {
269
- [f]:
270
- multiFacets.indexOf(f) > -1
271
- ? [locationSearchData[f]]
272
- : locationSearchData[f],
273
- }
274
- : {},
275
- ),
276
- ),
277
- );
258
+ const [facets, setFacets] = React.useState({});
259
+ const previousUrlQuery = usePrevious(urlQuery);
260
+
261
+ React.useEffect(() => {
262
+ if (!isEqual(urlQuery, previousUrlQuery)) {
263
+ setFacets(
264
+ Object.assign(
265
+ {},
266
+ ...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
267
+
268
+ // support for simple filters like ?Subject=something
269
+ // TODO: since the move to hash params this is no longer working.
270
+ // We'd have to treat the location.search and manage it just like the
271
+ // hash, to support it. We can read it, but we'd have to reset it as
272
+ // well, so at that point what's the difference to the hash?
273
+ ...configuredFacets.map((f) =>
274
+ locationSearchData[f]
275
+ ? {
276
+ [f]:
277
+ multiFacets.indexOf(f) > -1
278
+ ? [locationSearchData[f]]
279
+ : locationSearchData[f],
280
+ }
281
+ : {},
282
+ ),
283
+ ),
284
+ );
285
+ }
286
+ }, [
287
+ urlQuery,
288
+ configuredFacets,
289
+ locationSearchData,
290
+ multiFacets,
291
+ previousUrlQuery,
292
+ ]);
278
293
 
279
294
  const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
280
295
  const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
281
296
 
282
- const [searchData, setSearchData] = React.useState(
283
- getInitialState(data, facets, urlSearchText, id),
284
- );
297
+ const [searchData, setSearchData] = React.useState({});
298
+
299
+ React.useEffect(() => {
300
+ setSearchData(getInitialState(data, facets, urlSearchText, id));
301
+ }, [facets, data, urlSearchText, id]);
285
302
 
286
303
  const timeoutRef = React.useRef();
287
304
  const facetSettings = data?.facets;
@@ -16,16 +16,79 @@ import {
16
16
  getBlocksLayoutFieldname,
17
17
  } from '@plone/volto/helpers';
18
18
 
19
+ export const getBlocksTocEntries = (properties, tocData) => {
20
+ const blocksFieldName = getBlocksFieldname(properties);
21
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
22
+
23
+ const blocks = properties[blocksFieldName];
24
+ const blocks_layout = properties[blocksLayoutFieldname];
25
+
26
+ const levels =
27
+ tocData.levels?.length > 0
28
+ ? tocData.levels.map((l) => parseInt(l.slice(1)))
29
+ : [1, 2, 3, 4, 5, 6];
30
+ let rootLevel = Infinity;
31
+ let blocksFormEntries = [];
32
+ let tocEntries = {};
33
+ let tocEntriesLayout = [];
34
+
35
+ blocks_layout.items.forEach((id) => {
36
+ const block = blocks[id];
37
+ const blockConfig = config.blocks.blocksConfig[block['@type']];
38
+
39
+ if (!block || !blockConfig) {
40
+ return null;
41
+ }
42
+ if (!blockConfig.tocEntries && !blockConfig.tocEntry) {
43
+ return null;
44
+ }
45
+
46
+ const blockTocEntry = blockConfig.tocEntry?.(block, tocData);
47
+
48
+ const blockTocEntries = [
49
+ ...(blockConfig.tocEntries?.(block, tocData) ||
50
+ (blockTocEntry ? [blockTocEntry] : [])),
51
+ ];
52
+
53
+ blocksFormEntries = [...blocksFormEntries, ...blockTocEntries];
54
+
55
+ blockTocEntries.forEach((entry, index) => {
56
+ const i = `${id}-${index}`;
57
+ const level = entry[0];
58
+ const title = entry[1];
59
+ const items = [];
60
+ if (!level || !levels.includes(level)) return;
61
+ tocEntriesLayout.push(i);
62
+ tocEntries[i] = {
63
+ level,
64
+ title: title || block.plaintext,
65
+ items,
66
+ id: i,
67
+ };
68
+ if (level < rootLevel) {
69
+ rootLevel = level;
70
+ }
71
+ });
72
+ });
73
+
74
+ return {
75
+ rootLevel,
76
+ blocksFormEntries,
77
+ tocEntries,
78
+ tocEntriesLayout,
79
+ };
80
+ };
81
+
19
82
  /**
20
83
  * View toc block class.
21
84
  * @class View
22
85
  * @extends Component
23
86
  */
24
87
  const View = (props) => {
25
- const { properties, data } = props;
88
+ const { data } = props;
26
89
  const { variation } = props;
27
- const blocksFieldname = getBlocksFieldname(properties);
28
- const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
90
+ const metadata = props.metadata || props.properties;
91
+ const blocksFieldname = getBlocksFieldname(metadata);
29
92
  const levels = React.useMemo(
30
93
  () =>
31
94
  data.levels?.length > 0
@@ -34,14 +97,15 @@ const View = (props) => {
34
97
  [data],
35
98
  );
36
99
  const tocEntries = React.useMemo(() => {
37
- let rootLevel = Infinity;
38
100
  let entries = [];
39
101
  let prevEntry = {};
40
- let tocEntries = {};
41
- let tocEntriesLayout = [];
102
+ const { rootLevel, tocEntries, tocEntriesLayout } = getBlocksTocEntries(
103
+ metadata,
104
+ data,
105
+ );
42
106
 
43
- properties[blocksLayoutFieldname].items.forEach((id) => {
44
- const block = properties[blocksFieldname][id];
107
+ tocEntriesLayout.forEach((id) => {
108
+ const block = metadata[blocksFieldname][id];
45
109
  if (typeof block === 'undefined') {
46
110
  return null;
47
111
  }
@@ -50,6 +114,7 @@ const View = (props) => {
50
114
  block,
51
115
  data,
52
116
  );
117
+
53
118
  if (entry) {
54
119
  const level = entry[0];
55
120
  const title = entry[1];
@@ -64,9 +129,6 @@ const View = (props) => {
64
129
  override_toc: block.override_toc,
65
130
  plaintext: block.plaintext,
66
131
  };
67
- if (level < rootLevel) {
68
- rootLevel = level;
69
- }
70
132
  }
71
133
  });
72
134
 
@@ -98,7 +160,7 @@ const View = (props) => {
98
160
  });
99
161
 
100
162
  return entries;
101
- }, [data, levels, properties, blocksFieldname, blocksLayoutFieldname]);
163
+ }, [data, levels, metadata, blocksFieldname]);
102
164
 
103
165
  const Renderer = variation?.view;
104
166
  return (
@@ -108,7 +170,7 @@ const View = (props) => {
108
170
  )}
109
171
 
110
172
  {Renderer ? (
111
- <Renderer {...props} tocEntries={tocEntries} properties={properties} />
173
+ <Renderer {...props} tocEntries={tocEntries} metadata={metadata} />
112
174
  ) : (
113
175
  <div>View extension not found</div>
114
176
  )}
@@ -8,13 +8,10 @@ import PropTypes from 'prop-types';
8
8
  import { map } from 'lodash';
9
9
  import { List } from 'semantic-ui-react';
10
10
  import { FormattedMessage, injectIntl } from 'react-intl';
11
- import { useHistory } from 'react-router-dom';
12
- import AnchorLink from 'react-anchor-link-smooth-scroll';
13
11
  import Slugger from 'github-slugger';
12
+ import { UniversalLink } from '@plone/volto/components';
14
13
 
15
14
  const RenderListItems = ({ items, data }) => {
16
- const history = useHistory();
17
-
18
15
  return map(items, (item) => {
19
16
  const { id, level, title, override_toc, plaintext } = item;
20
17
  const slug = override_toc
@@ -23,14 +20,7 @@ const RenderListItems = ({ items, data }) => {
23
20
  return (
24
21
  item && (
25
22
  <List.Item key={id} className={`item headline-${level}`} as="li">
26
- <AnchorLink
27
- href={`#${slug}`}
28
- onClick={(e) => {
29
- history.push({ hash: slug });
30
- }}
31
- >
32
- {title}
33
- </AnchorLink>
23
+ <UniversalLink href={`#${slug}`}>{title}</UniversalLink>
34
24
  {item.items?.length > 0 && (
35
25
  <List
36
26
  ordered={data.ordered}
@@ -96,6 +96,7 @@ class GroupsControlpanel extends Component {
96
96
  this.updateGroupRole = this.updateGroupRole.bind(this);
97
97
  this.state = {
98
98
  search: '',
99
+ isLoading: false,
99
100
  addGroupError: '',
100
101
  showDelete: false,
101
102
  groupToDelete: undefined,
@@ -173,8 +174,18 @@ class GroupsControlpanel extends Component {
173
174
  * @returns {undefined}
174
175
  */
175
176
  onSearchGroups(event) {
177
+ this.setState({ isLoading: true });
176
178
  event.preventDefault();
177
- this.props.listGroups(this.state.search);
179
+ this.props
180
+ .listGroups(this.state.search)
181
+ .then(() => {
182
+ this.setState({ isLoading: false });
183
+ })
184
+ .catch((error) => {
185
+ this.setState({ isLoading: false });
186
+ // eslint-disable-next-line no-console
187
+ console.error('Error searching group', error);
188
+ });
178
189
  }
179
190
 
180
191
  /**
@@ -487,7 +498,11 @@ class GroupsControlpanel extends Component {
487
498
  <Form.Field>
488
499
  <Input
489
500
  name="SearchableText"
490
- action={{ icon: 'search' }}
501
+ action={{
502
+ icon: 'search',
503
+ loading: this.state.isLoading,
504
+ disabled: this.state.isLoading,
505
+ }}
491
506
  placeholder={this.props.intl.formatMessage(
492
507
  messages.searchGroups,
493
508
  )}
@@ -499,43 +514,55 @@ class GroupsControlpanel extends Component {
499
514
  </Segment>
500
515
  <Form>
501
516
  <div className="table">
502
- <Table padded striped attached unstackable>
503
- <Table.Header>
504
- <Table.Row>
505
- <Table.HeaderCell>
506
- <FormattedMessage
507
- id="Groupname"
508
- defaultMessage="Groupname"
509
- />
510
- </Table.HeaderCell>
511
- {this.props.roles.map((role) => (
512
- <Table.HeaderCell key={role.id}>
513
- {role.title}
517
+ {((this.props.many_groups &&
518
+ this.state.groupEntries.length > 0) ||
519
+ !this.props.many_groups) && (
520
+ <Table padded striped attached unstackable>
521
+ <Table.Header>
522
+ <Table.Row>
523
+ <Table.HeaderCell>
524
+ <FormattedMessage
525
+ id="Groupname"
526
+ defaultMessage="Groupname"
527
+ />
528
+ </Table.HeaderCell>
529
+ {this.props.roles.map((role) => (
530
+ <Table.HeaderCell key={role.id}>
531
+ {role.title}
532
+ </Table.HeaderCell>
533
+ ))}
534
+ <Table.HeaderCell>
535
+ <FormattedMessage
536
+ id="Actions"
537
+ defaultMessage="Actions"
538
+ />
514
539
  </Table.HeaderCell>
515
- ))}
516
- <Table.HeaderCell>
517
- <FormattedMessage id="Actions" defaultMessage="Actions" />
518
- </Table.HeaderCell>
519
- </Table.Row>
520
- </Table.Header>
521
- <Table.Body data-group="groups">
522
- {this.state.groupEntries
523
- .slice(
524
- this.state.currentPage * 10,
525
- this.state.pageSize * (this.state.currentPage + 1),
526
- )
527
- .map((group) => (
528
- <RenderGroups
529
- key={group.id}
530
- onDelete={this.deleteGroup}
531
- roles={this.props.roles}
532
- group={group}
533
- updateGroups={this.updateGroupRole}
534
- inheritedRole={this.state.authenticatedRole}
535
- />
536
- ))}
537
- </Table.Body>
538
- </Table>
540
+ </Table.Row>
541
+ </Table.Header>
542
+ <Table.Body data-group="groups">
543
+ {this.state.groupEntries
544
+ .slice(
545
+ this.state.currentPage * 10,
546
+ this.state.pageSize * (this.state.currentPage + 1),
547
+ )
548
+ .map((group) => (
549
+ <RenderGroups
550
+ key={group.id}
551
+ onDelete={this.deleteGroup}
552
+ roles={this.props.roles}
553
+ group={group}
554
+ updateGroups={this.updateGroupRole}
555
+ inheritedRole={this.state.authenticatedRole}
556
+ />
557
+ ))}
558
+ </Table.Body>
559
+ </Table>
560
+ )}
561
+ {this.state.groupEntries.length === 0 && this.state.search && (
562
+ <Segment>
563
+ {this.props.intl.formatMessage(messages.groupSearchNoResults)}
564
+ </Segment>
565
+ )}
539
566
  </div>
540
567
  <div className="contents-pagination">
541
568
  <Pagination
@@ -179,7 +179,7 @@ class AddRule extends Component {
179
179
  const { title, description, event, cascading, stop, enabled } = this.state;
180
180
  const triggeringEvents =
181
181
  this.props.events?.items && this.props.events?.items.length > 0
182
- ? this.props.events?.items.map((event) => [event.title, event.token])
182
+ ? this.props.events?.items.map((event) => [event.token, event.title])
183
183
  : '';
184
184
 
185
185
  return (
@@ -209,7 +209,7 @@ class EditRule extends Component {
209
209
 
210
210
  const triggeringEvents =
211
211
  this.props.events?.items && this.props.events?.items.length > 0
212
- ? this.props.events?.items.map((event) => [event.title, event.token])
212
+ ? this.props.events?.items.map((event) => [event.token, event.title])
213
213
  : '';
214
214
 
215
215
  return (
@@ -7,8 +7,14 @@ import React, { Component } from 'react';
7
7
  import { FormattedMessage, injectIntl } from 'react-intl';
8
8
  import { Dropdown, Table, Checkbox } from 'semantic-ui-react';
9
9
  import trashSVG from '@plone/volto/icons/delete.svg';
10
- import { Icon } from '@plone/volto/components';
10
+ import editSVG from '@plone/volto/icons/editing.svg';
11
+ import { Icon, ModalForm, Toast } from '@plone/volto/components';
12
+ import { updateUser } from '@plone/volto/actions';
11
13
  import ploneSVG from '@plone/volto/icons/plone.svg';
14
+ import { compose } from 'redux';
15
+ import { connect } from 'react-redux';
16
+ import { messages } from '@plone/volto/helpers';
17
+ import { toast } from 'react-toastify';
12
18
 
13
19
  /**
14
20
  * UsersControlpanelUser class.
@@ -43,9 +49,14 @@ class RenderUsers extends Component {
43
49
  */
44
50
  constructor(props) {
45
51
  super(props);
46
- this.state = {};
52
+ this.state = {
53
+ user: {},
54
+ };
47
55
  this.onChange = this.onChange.bind(this);
56
+ this.onEditUserError = this.onEditUserError.bind(this);
57
+ this.onEditUserSubmit = this.onEditUserSubmit.bind(this);
48
58
  }
59
+
49
60
  /**
50
61
  * @param {*} event
51
62
  * @param {*} { value }
@@ -56,6 +67,49 @@ class RenderUsers extends Component {
56
67
  const [user, role] = value.split('&role=');
57
68
  this.props.updateUser(user, role);
58
69
  }
70
+
71
+ componentDidUpdate(prevProps) {
72
+ if (
73
+ prevProps.updateRequest.loading &&
74
+ this.props.updateRequest.loaded &&
75
+ this.state?.user?.id === this.props?.user?.id
76
+ ) {
77
+ this.setState({ user: {} });
78
+ this.props.listUsers();
79
+ return toast.success(
80
+ <Toast
81
+ success
82
+ title={this.props.intl.formatMessage(messages.success)}
83
+ content={this.props.intl.formatMessage(messages.updateUserSuccess)}
84
+ />,
85
+ );
86
+ }
87
+ }
88
+
89
+ onEditUserSubmit(data, callback) {
90
+ // Do not handle groups and roles in this form
91
+ delete data.groups;
92
+ delete data.roles;
93
+ this.props.updateUserData(data.id, data);
94
+ }
95
+
96
+ onEditUserError() {
97
+ return toast.error(
98
+ <Toast
99
+ error
100
+ title={this.props.intl.formatMessage(messages.error)}
101
+ content={this.props.intl.formatMessage(
102
+ messages.addUserFormPasswordAndSendPasswordTogetherNotAllowed,
103
+ )}
104
+ />,
105
+ );
106
+ }
107
+
108
+ onClickEdit(props) {
109
+ const { formData } = props;
110
+ this.setState({ user: { ...formData } });
111
+ }
112
+
59
113
  /**
60
114
  * Render method.
61
115
  * @method render
@@ -67,7 +121,8 @@ class RenderUsers extends Component {
67
121
  <Table.Cell className="fullname">
68
122
  {this.props.user.fullname
69
123
  ? this.props.user.fullname
70
- : this.props.user.username}
124
+ : this.props.user.username}{' '}
125
+ ({this.props.user.username})
71
126
  </Table.Cell>
72
127
  {this.props.roles.map((role) => (
73
128
  <Table.Cell key={role.id}>
@@ -91,7 +146,20 @@ class RenderUsers extends Component {
91
146
  <Table.Cell textAlign="right">
92
147
  <Dropdown icon="ellipsis horizontal">
93
148
  <Dropdown.Menu className="left">
149
+ {this.props.userschema && (
150
+ <Dropdown.Item
151
+ id="edit-user-button"
152
+ onClick={() => {
153
+ this.onClickEdit({ formData: this.props.user });
154
+ }}
155
+ value={this.props.user['@id']}
156
+ >
157
+ <Icon name={editSVG} size="15px" />
158
+ <FormattedMessage id="Edit" defaultMessage="Edit" />
159
+ </Dropdown.Item>
160
+ )}
94
161
  <Dropdown.Item
162
+ id="delete-user-button"
95
163
  onClick={this.props.onDelete}
96
164
  value={this.props.user['@id']}
97
165
  >
@@ -101,9 +169,31 @@ class RenderUsers extends Component {
101
169
  </Dropdown.Menu>
102
170
  </Dropdown>
103
171
  </Table.Cell>
172
+ {Object.keys(this.state.user).length > 0 &&
173
+ this.props.userschema.loaded && (
174
+ <ModalForm
175
+ className="modal"
176
+ onSubmit={this.onEditUserSubmit}
177
+ submitError={this.state.editUserError}
178
+ formData={this.state.user}
179
+ onCancel={() => this.setState({ user: {} })}
180
+ title={this.props.intl.formatMessage(
181
+ messages.updateUserFormTitle,
182
+ )}
183
+ loading={this.props.updateRequest.loading}
184
+ schema={this.props.userschema.userschema}
185
+ />
186
+ )}
104
187
  </Table.Row>
105
188
  );
106
189
  }
107
190
  }
108
-
109
- export default injectIntl(RenderUsers);
191
+ export default compose(
192
+ injectIntl,
193
+ connect(
194
+ (state, props) => ({
195
+ updateRequest: state.users?.update,
196
+ }),
197
+ { updateUserData: updateUser },
198
+ ),
199
+ )(RenderUsers);