@plone-collective/volto-identity 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/.changelog.draft +261 -0
  2. package/.release-it.json +34 -0
  3. package/CHANGELOG.md +269 -0
  4. package/LICENSE +21 -0
  5. package/README.md +228 -0
  6. package/babel.config.js +17 -0
  7. package/locales/de/LC_MESSAGES/volto.po +1162 -0
  8. package/locales/en/LC_MESSAGES/volto.po +1162 -0
  9. package/locales/es/LC_MESSAGES/volto.po +1169 -0
  10. package/locales/pt_BR/LC_MESSAGES/volto.po +1167 -0
  11. package/locales/volto.pot +1164 -0
  12. package/news/.changelog_template.jinja +15 -0
  13. package/news/.gitkeep +0 -0
  14. package/package.json +66 -0
  15. package/public/.gitkeep +0 -0
  16. package/src/actions/actions.test.ts +226 -0
  17. package/src/actions/index.ts +530 -0
  18. package/src/components/.gitkeep +0 -0
  19. package/src/components/Applications/Applications.tsx +169 -0
  20. package/src/components/Applications/ApplicationsPanel.scss +59 -0
  21. package/src/components/Applications/ApplicationsPanel.stories.tsx +82 -0
  22. package/src/components/Applications/ApplicationsPanel.test.tsx +235 -0
  23. package/src/components/Applications/ApplicationsPanel.tsx +342 -0
  24. package/src/components/Callback/Callback.scss +17 -0
  25. package/src/components/Callback/Callback.stories.tsx +43 -0
  26. package/src/components/Callback/Callback.test.tsx +262 -0
  27. package/src/components/Callback/Callback.tsx +170 -0
  28. package/src/components/Consent/Consent.test.tsx +75 -0
  29. package/src/components/Consent/Consent.tsx +98 -0
  30. package/src/components/Consent/ConsentPanel.scss +37 -0
  31. package/src/components/Consent/ConsentPanel.stories.tsx +80 -0
  32. package/src/components/Consent/ConsentPanel.test.tsx +138 -0
  33. package/src/components/Consent/ConsentPanel.tsx +162 -0
  34. package/src/components/ControlPanel/ClientsControlPanel.stories.tsx +38 -0
  35. package/src/components/ControlPanel/ClientsControlPanel.tsx +321 -0
  36. package/src/components/ControlPanel/ClientsPanel.scss +21 -0
  37. package/src/components/ControlPanel/ClientsPanel.stories.tsx +71 -0
  38. package/src/components/ControlPanel/ClientsPanel.test.tsx +267 -0
  39. package/src/components/ControlPanel/ClientsPanel.tsx +386 -0
  40. package/src/components/ControlPanel/ConfirmModal.stories.tsx +35 -0
  41. package/src/components/ControlPanel/ConfirmModal.test.tsx +66 -0
  42. package/src/components/ControlPanel/ConfirmModal.tsx +68 -0
  43. package/src/components/ControlPanel/ProviderForm.stories.tsx +107 -0
  44. package/src/components/ControlPanel/ProvidersControlPanel.scss +26 -0
  45. package/src/components/ControlPanel/ProvidersControlPanel.stories.tsx +88 -0
  46. package/src/components/ControlPanel/ProvidersControlPanel.tsx +600 -0
  47. package/src/components/ControlPanel/RenderUsers.stories.tsx +112 -0
  48. package/src/components/ControlPanel/RenderUsers.test.tsx +145 -0
  49. package/src/components/ControlPanel/RenderUsers.tsx +263 -0
  50. package/src/components/ControlPanel/SecretReveal.scss +32 -0
  51. package/src/components/ControlPanel/SecretReveal.stories.tsx +33 -0
  52. package/src/components/ControlPanel/SecretReveal.test.tsx +101 -0
  53. package/src/components/ControlPanel/SecretReveal.tsx +105 -0
  54. package/src/components/ControlPanel/UserAccount.stories.tsx +69 -0
  55. package/src/components/ControlPanel/UserAccount.test.tsx +117 -0
  56. package/src/components/ControlPanel/UserAccount.tsx +125 -0
  57. package/src/components/ControlPanel/UserAccountPanel.scss +38 -0
  58. package/src/components/ControlPanel/UserAccountPanel.stories.tsx +77 -0
  59. package/src/components/ControlPanel/UserAccountPanel.test.tsx +173 -0
  60. package/src/components/ControlPanel/UserAccountPanel.tsx +273 -0
  61. package/src/components/FirstLogin/FirstLogin.scss +12 -0
  62. package/src/components/FirstLogin/FirstLogin.stories.tsx +50 -0
  63. package/src/components/FirstLogin/FirstLogin.tsx +94 -0
  64. package/src/components/Identities/Identities.stories.tsx +51 -0
  65. package/src/components/Identities/Identities.test.tsx +134 -0
  66. package/src/components/Identities/Identities.tsx +217 -0
  67. package/src/components/Identities/IdentitiesList.scss +49 -0
  68. package/src/components/Identities/IdentitiesList.stories.tsx +71 -0
  69. package/src/components/Identities/IdentitiesList.test.tsx +218 -0
  70. package/src/components/Identities/IdentitiesList.tsx +207 -0
  71. package/src/components/Identities/ProfileEmails.scss +62 -0
  72. package/src/components/Identities/ProfileEmails.stories.tsx +75 -0
  73. package/src/components/Identities/ProfileEmails.test.tsx +132 -0
  74. package/src/components/Identities/ProfileEmails.tsx +217 -0
  75. package/src/components/Login/Login.stories.tsx +63 -0
  76. package/src/components/Login/Login.test.tsx +189 -0
  77. package/src/components/Login/Login.tsx +163 -0
  78. package/src/components/Login/LoginForm.scss +39 -0
  79. package/src/components/Login/LoginForm.stories.tsx +112 -0
  80. package/src/components/Login/LoginForm.test.tsx +483 -0
  81. package/src/components/Login/LoginForm.tsx +321 -0
  82. package/src/components/Login/LoginOverlay.scss +44 -0
  83. package/src/components/Login/LoginOverlay.stories.tsx +73 -0
  84. package/src/components/Login/LoginOverlay.test.tsx +98 -0
  85. package/src/components/Login/LoginOverlay.tsx +107 -0
  86. package/src/components/Login/LoginPanel.scss +50 -0
  87. package/src/components/Login/LoginPanel.stories.tsx +40 -0
  88. package/src/components/Login/LoginPanel.tsx +60 -0
  89. package/src/components/Login/MagicLinkForm.scss +25 -0
  90. package/src/components/Login/MagicLinkForm.stories.tsx +27 -0
  91. package/src/components/Login/MagicLinkForm.tsx +133 -0
  92. package/src/components/Login/PasswordForm.scss +82 -0
  93. package/src/components/Login/PasswordForm.stories.tsx +26 -0
  94. package/src/components/Login/PasswordForm.tsx +159 -0
  95. package/src/components/Login/ProviderButton.scss +103 -0
  96. package/src/components/Login/ProviderButton.stories.tsx +74 -0
  97. package/src/components/Login/ProviderButton.test.tsx +99 -0
  98. package/src/components/Login/ProviderButton.tsx +97 -0
  99. package/src/components/ProfileGate/ProfileGate.stories.tsx +118 -0
  100. package/src/components/ProfileGate/ProfileGate.test.tsx +370 -0
  101. package/src/components/ProfileGate/ProfileGate.tsx +172 -0
  102. package/src/components/Toolbar/PersonalTools.stories.tsx +111 -0
  103. package/src/components/Toolbar/PersonalTools.test.tsx +165 -0
  104. package/src/components/Toolbar/PersonalTools.tsx +177 -0
  105. package/src/components/Toolbar/Toolbar.jsx +766 -0
  106. package/src/components/UserMenu/ApplicationsMenuItem.stories.tsx +62 -0
  107. package/src/components/UserMenu/ApplicationsMenuItem.test.tsx +109 -0
  108. package/src/components/UserMenu/ApplicationsMenuItem.tsx +82 -0
  109. package/src/components/UserMenu/IdentitiesMenuItem.stories.tsx +35 -0
  110. package/src/components/UserMenu/IdentitiesMenuItem.test.tsx +103 -0
  111. package/src/components/UserMenu/IdentitiesMenuItem.tsx +34 -0
  112. package/src/components/UserMenu/MenuItem.stories.tsx +32 -0
  113. package/src/components/UserMenu/MenuItem.tsx +48 -0
  114. package/src/components/UserMenu/ProfileMenuItem.stories.tsx +76 -0
  115. package/src/components/UserMenu/ProfileMenuItem.test.tsx +90 -0
  116. package/src/components/UserMenu/ProfileMenuItem.tsx +60 -0
  117. package/src/components/UserMenu/UserAvatar.scss +23 -0
  118. package/src/components/UserMenu/UserAvatar.stories.tsx +121 -0
  119. package/src/components/UserMenu/UserAvatar.test.tsx +120 -0
  120. package/src/components/UserMenu/UserAvatar.tsx +119 -0
  121. package/src/components/UserMenu/UserMenuPlugs.stories.tsx +101 -0
  122. package/src/components/UserMenu/UserMenuPlugs.test.tsx +202 -0
  123. package/src/components/UserMenu/UserMenuPlugs.tsx +142 -0
  124. package/src/components/UserMenu/UserProfileLoader.test.tsx +211 -0
  125. package/src/components/UserMenu/UserProfileLoader.tsx +54 -0
  126. package/src/components/Views/GroupView.scss +36 -0
  127. package/src/components/Views/GroupView.stories.tsx +98 -0
  128. package/src/components/Views/GroupView.test.tsx +137 -0
  129. package/src/components/Views/GroupView.tsx +178 -0
  130. package/src/components/Views/ProfileView.scss +27 -0
  131. package/src/components/Views/ProfileView.stories.tsx +34 -0
  132. package/src/components/Views/ProfileView.test.tsx +82 -0
  133. package/src/components/Views/ProfileView.tsx +100 -0
  134. package/src/components/Widgets/ProviderIconWidget.scss +21 -0
  135. package/src/components/Widgets/ProviderIconWidget.stories.tsx +30 -0
  136. package/src/components/Widgets/ProviderIconWidget.test.tsx +55 -0
  137. package/src/components/Widgets/ProviderIconWidget.tsx +143 -0
  138. package/src/config/menu.ts +35 -0
  139. package/src/config/reducers.ts +7 -0
  140. package/src/config/routes.test.ts +177 -0
  141. package/src/config/routes.ts +143 -0
  142. package/src/config/settings.test.ts +34 -0
  143. package/src/config/settings.ts +72 -0
  144. package/src/config/views.ts +30 -0
  145. package/src/config/widgets.ts +25 -0
  146. package/src/constants/ActionTypes.ts +42 -0
  147. package/src/constants/vocabularies.ts +20 -0
  148. package/src/customizations/volto/components/manage/Controlpanels/Users/RenderUsers.tsx +23 -0
  149. package/src/customizations/volto/components/manage/Toolbar/PersonalTools.tsx +21 -0
  150. package/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx +21 -0
  151. package/src/helpers/avatar.test.ts +79 -0
  152. package/src/helpers/avatar.ts +85 -0
  153. package/src/helpers/callback.test.ts +60 -0
  154. package/src/helpers/callback.ts +71 -0
  155. package/src/helpers/clientSchema.test.ts +182 -0
  156. package/src/helpers/clientSchema.ts +181 -0
  157. package/src/helpers/firstLogin.test.ts +90 -0
  158. package/src/helpers/firstLogin.ts +76 -0
  159. package/src/helpers/groupmap.test.ts +81 -0
  160. package/src/helpers/groupmap.ts +50 -0
  161. package/src/helpers/identities.ts +22 -0
  162. package/src/helpers/navigate.test.ts +132 -0
  163. package/src/helpers/navigate.ts +126 -0
  164. package/src/helpers/profileGate.test.ts +150 -0
  165. package/src/helpers/profileGate.ts +191 -0
  166. package/src/helpers/profileSource.ts +41 -0
  167. package/src/helpers/propertymap.test.ts +71 -0
  168. package/src/helpers/propertymap.ts +43 -0
  169. package/src/helpers/providerSchema.test.ts +410 -0
  170. package/src/helpers/providerSchema.ts +362 -0
  171. package/src/helpers/returnUrl.test.ts +42 -0
  172. package/src/helpers/returnUrl.ts +41 -0
  173. package/src/helpers/rowmap.ts +110 -0
  174. package/src/helpers/showPloneLogin.test.ts +78 -0
  175. package/src/helpers/showPloneLogin.ts +55 -0
  176. package/src/helpers/token.test.ts +62 -0
  177. package/src/helpers/token.ts +71 -0
  178. package/src/index.ts +22 -0
  179. package/src/reducers/index.ts +369 -0
  180. package/src/reducers/reducers.test.ts +369 -0
  181. package/src/stories/fixtures.tsx +640 -0
  182. package/src/storybook/withPage.test.tsx +90 -0
  183. package/src/storybook/withPage.tsx +110 -0
  184. package/src/storybook/withUserMenu.test.tsx +78 -0
  185. package/src/storybook/withUserMenu.tsx +136 -0
  186. package/src/styles.css +262 -0
  187. package/src/testing.tsx +86 -0
  188. package/src/types.ts +441 -0
  189. package/towncrier.toml +38 -0
  190. package/tsconfig.json +22 -0
  191. package/vitest.config.mjs +47 -0
@@ -0,0 +1,766 @@
1
+ /**
2
+ * COPIED FROM VOLTO 19.3.0.
3
+ *
4
+ * Takes Volto's place through
5
+ * `customizations/volto/components/manage/Toolbar/Toolbar`, which is where
6
+ * the reason for shadowing it is written down.
7
+ *
8
+ * This file is a verbatim copy of
9
+ * `@plone/volto/components/manage/Toolbar/Toolbar.jsx` with the changes
10
+ * listed below and nothing else. Keep it that way: when Volto is upgraded,
11
+ * diff the upstream file against this one, take upstream's version wholesale
12
+ * and re-apply only these changes. It is the one component here that has not
13
+ * been rewritten as ours -- until it is, `.jsx` and a literal diff are what
14
+ * make the upgrade tractable.
15
+ *
16
+ * WHAT CHANGED:
17
+ * - the `#toolbar-personal` button renders `<UserAvatar>` instead of
18
+ * the generic `user.svg` icon, so the button shows *who* is signed
19
+ * in rather than that somebody is;
20
+ * - the now-unused `userSVG` import is replaced by that component's.
21
+ */
22
+ /**
23
+ * Toolbar component.
24
+ * @module components/manage/Toolbar/Toolbar
25
+ */
26
+
27
+ import React, { Component } from 'react';
28
+ import { defineMessages, injectIntl } from 'react-intl';
29
+ import PropTypes from 'prop-types';
30
+ import { Link } from 'react-router-dom';
31
+ import jwtDecode from 'jwt-decode';
32
+ import { connect } from 'react-redux';
33
+ import { compose } from 'redux';
34
+ import doesNodeContainClick from 'semantic-ui-react/dist/commonjs/lib/doesNodeContainClick';
35
+ import { withCookies } from 'react-cookie';
36
+ import filter from 'lodash/filter';
37
+ import find from 'lodash/find';
38
+ import cx from 'classnames';
39
+ import config from '@plone/volto/registry';
40
+
41
+ import More from '@plone/volto/components/manage/Toolbar/More';
42
+ import PersonalTools from '@plone/volto/components/manage/Toolbar/PersonalTools';
43
+ import Types from '@plone/volto/components/manage/Toolbar/Types';
44
+ import PersonalInformation from '@plone/volto/components/manage/Preferences/PersonalInformation';
45
+ import PersonalPreferences from '@plone/volto/components/manage/Preferences/PersonalPreferences';
46
+ import StandardWrapper from '@plone/volto/components/manage/Toolbar/StandardWrapper';
47
+ import { getTypes } from '@plone/volto/actions/types/types';
48
+ import { listActions } from '@plone/volto/actions/actions/actions';
49
+ import { setExpandedToolbar } from '@plone/volto/actions/toolbar/toolbar';
50
+ import { unlockContent } from '@plone/volto/actions/content/content';
51
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
52
+ import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
53
+ import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
54
+ import { getCookieOptions } from '@plone/volto/helpers/Cookies/cookies';
55
+ import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
56
+ import { Pluggable } from '@plone/volto/components/manage/Pluggable';
57
+
58
+ import penSVG from '@plone/volto/icons/pen.svg';
59
+ import unlockSVG from '@plone/volto/icons/unlock.svg';
60
+ import folderSVG from '@plone/volto/icons/folder.svg';
61
+ import addSVG from '@plone/volto/icons/add-document.svg';
62
+ import moreSVG from '@plone/volto/icons/more.svg';
63
+ import UserAvatar from '@plone-collective/volto-identity/components/UserMenu/UserAvatar';
64
+ import backSVG from '@plone/volto/icons/back.svg';
65
+ import clearSVG from '@plone/volto/icons/clear.svg';
66
+
67
+ const messages = defineMessages({
68
+ edit: {
69
+ id: 'Edit',
70
+ defaultMessage: 'Edit',
71
+ },
72
+ contents: {
73
+ id: 'Contents',
74
+ defaultMessage: 'Contents',
75
+ },
76
+ add: {
77
+ id: 'Add',
78
+ defaultMessage: 'Add',
79
+ },
80
+ more: {
81
+ id: 'More',
82
+ defaultMessage: 'More',
83
+ },
84
+ personalTools: {
85
+ id: 'Personal tools',
86
+ defaultMessage: 'Personal tools',
87
+ },
88
+ shrinkToolbar: {
89
+ id: 'Shrink toolbar',
90
+ defaultMessage: 'Shrink toolbar',
91
+ },
92
+ expandToolbar: {
93
+ id: 'Expand toolbar',
94
+ defaultMessage: 'Expand toolbar',
95
+ },
96
+ personalInformation: {
97
+ id: 'Personal Information',
98
+ defaultMessage: 'Personal Information',
99
+ },
100
+ personalPreferences: {
101
+ id: 'Personal Preferences',
102
+ defaultMessage: 'Personal Preferences',
103
+ },
104
+ collection: {
105
+ id: 'Collection',
106
+ defaultMessage: 'Collection',
107
+ },
108
+ file: {
109
+ id: 'File',
110
+ defaultMessage: 'File',
111
+ },
112
+ link: {
113
+ id: 'Link',
114
+ defaultMessage: 'Link',
115
+ },
116
+ newsItem: {
117
+ id: 'News Item',
118
+ defaultMessage: 'News Item',
119
+ },
120
+ page: {
121
+ id: 'Page',
122
+ defaultMessage: 'Page',
123
+ },
124
+ back: {
125
+ id: 'Back',
126
+ defaultMessage: 'Back',
127
+ },
128
+ unlock: {
129
+ id: 'Unlock',
130
+ defaultMessage: 'Unlock',
131
+ },
132
+ menuOpened: {
133
+ id: 'Menu opened',
134
+ defaultMessage: 'Menu opened',
135
+ },
136
+ menuClosed: {
137
+ id: 'Menu closed',
138
+ defaultMessage: 'Menu closed',
139
+ },
140
+ focusOn: {
141
+ id: 'Focus on',
142
+ defaultMessage: 'Focus on',
143
+ },
144
+ });
145
+
146
+ let toolbarComponents = {
147
+ personalTools: { component: PersonalTools, wrapper: null },
148
+ more: { component: More, wrapper: null },
149
+ types: { component: Types, wrapper: null, contentAsProps: true },
150
+ profile: {
151
+ component: PersonalInformation,
152
+ wrapper: StandardWrapper,
153
+ wrapperTitle: messages.personalInformation,
154
+ hideToolbarBody: true,
155
+ },
156
+ preferences: {
157
+ component: PersonalPreferences,
158
+ wrapper: StandardWrapper,
159
+ wrapperTitle: messages.personalPreferences,
160
+ hideToolbarBody: true,
161
+ },
162
+ };
163
+
164
+ /**
165
+ * Toolbar container class.
166
+ * @class Toolbar
167
+ * @extends Component
168
+ */
169
+ class Toolbar extends Component {
170
+ /**
171
+ * Property types.
172
+ * @property {Object} propTypes Property types.
173
+ * @static
174
+ */
175
+ static propTypes = {
176
+ actions: PropTypes.shape({
177
+ object: PropTypes.arrayOf(PropTypes.object),
178
+ object_buttons: PropTypes.arrayOf(PropTypes.object),
179
+ user: PropTypes.arrayOf(PropTypes.object),
180
+ }),
181
+ token: PropTypes.string,
182
+ userId: PropTypes.string,
183
+ pathname: PropTypes.string.isRequired,
184
+ content: PropTypes.shape({
185
+ '@type': PropTypes.string,
186
+ is_folderish: PropTypes.bool,
187
+ review_state: PropTypes.string,
188
+ }),
189
+ getTypes: PropTypes.func.isRequired,
190
+ types: PropTypes.arrayOf(
191
+ PropTypes.shape({
192
+ '@id': PropTypes.string,
193
+ addable: PropTypes.bool,
194
+ title: PropTypes.string,
195
+ }),
196
+ ),
197
+ listActions: PropTypes.func.isRequired,
198
+ unlockContent: PropTypes.func,
199
+ unlockRequest: PropTypes.objectOf(PropTypes.any),
200
+ inner: PropTypes.element.isRequired,
201
+ hideDefaultViewButtons: PropTypes.bool,
202
+ };
203
+
204
+ /**
205
+ * Default properties.
206
+ * @property {Object} defaultProps Default properties.
207
+ * @static
208
+ */
209
+ static defaultProps = {
210
+ actions: null,
211
+ token: null,
212
+ userId: null,
213
+ content: null,
214
+ hideDefaultViewButtons: false,
215
+ types: [],
216
+ };
217
+
218
+ toolbarRef = React.createRef();
219
+ toolbarWindow = React.createRef();
220
+ buttonRef = React.createRef();
221
+ announceRef = React.createRef();
222
+
223
+ constructor(props) {
224
+ super(props);
225
+ const { cookies } = props;
226
+ this.state = {
227
+ expanded: cookies.get('toolbar_expanded') !== 'false',
228
+ showMenu: false,
229
+ menuStyle: {},
230
+ menuComponents: [],
231
+ loadedComponents: [],
232
+ hideToolbarBody: false,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Component will mount
238
+ * @method componentDidMount
239
+ * @returns {undefined}
240
+ */
241
+ componentDidMount() {
242
+ // Do not trigger the actions action if the expander is present
243
+ if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) {
244
+ this.props.listActions(getBaseUrl(this.props.pathname));
245
+ }
246
+ // Do not trigger the types action if the expander is present
247
+ if (!hasApiExpander('types', getBaseUrl(this.props.pathname))) {
248
+ this.props.getTypes(getBaseUrl(this.props.pathname));
249
+ }
250
+ toolbarComponents = {
251
+ ...(config.settings
252
+ ? config.settings.additionalToolbarComponents || {}
253
+ : {}),
254
+ ...toolbarComponents,
255
+ };
256
+ this.props.setExpandedToolbar(this.state.expanded);
257
+ document.addEventListener('mousedown', this.handleClickOutside, false);
258
+ }
259
+
260
+ /**
261
+ * Component will receive props
262
+ * @method componentWillReceiveProps
263
+ * @param {Object} nextProps Next properties
264
+ * @returns {undefined}
265
+ */
266
+ UNSAFE_componentWillReceiveProps(nextProps) {
267
+ if (nextProps.pathname !== this.props.pathname) {
268
+ // Do not trigger the actions action if the expander is present
269
+ if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) {
270
+ this.props.listActions(getBaseUrl(nextProps.pathname));
271
+ }
272
+ // Do not trigger the types action if the expander is present
273
+ if (!hasApiExpander('types', getBaseUrl(nextProps.pathname))) {
274
+ this.props.getTypes(getBaseUrl(nextProps.pathname));
275
+ }
276
+ }
277
+
278
+ // Unlock
279
+ if (this.props.unlockRequest.loading && nextProps.unlockRequest.loaded) {
280
+ this.props.listActions(getBaseUrl(nextProps.pathname));
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Component will receive props
286
+ * @method componentWillUnmount
287
+ * @returns {undefined}
288
+ */
289
+ componentWillUnmount() {
290
+ document.removeEventListener('mousedown', this.handleClickOutside, false);
291
+ }
292
+
293
+ handleShrink = () => {
294
+ const { cookies } = this.props;
295
+ cookies.set('toolbar_expanded', !this.state.expanded, getCookieOptions());
296
+ this.setState(
297
+ (state) => ({ expanded: !state.expanded }),
298
+ () => this.props.setExpandedToolbar(this.state.expanded),
299
+ );
300
+ };
301
+
302
+ closeMenu = () => {
303
+ this.setState(() => ({ showMenu: false, loadedComponents: [] }));
304
+ };
305
+
306
+ loadComponent = (type) => {
307
+ const { loadedComponents } = this.state;
308
+ if (!this.state.loadedComponents.includes(type)) {
309
+ this.setState({
310
+ loadedComponents: [...loadedComponents, type],
311
+ hideToolbarBody: toolbarComponents[type].hideToolbarBody || false,
312
+ });
313
+ }
314
+ };
315
+
316
+ unloadComponent = () => {
317
+ this.setState((state) => ({
318
+ loadedComponents: state.loadedComponents.slice(0, -1),
319
+ hideToolbarBody:
320
+ toolbarComponents[
321
+ state.loadedComponents[state.loadedComponents.length - 2]
322
+ ].hideToolbarBody || false,
323
+ }));
324
+ };
325
+
326
+ toggleButtonPressed = (e) => {
327
+ const target = e.target;
328
+ const button =
329
+ target.tagName === 'BUTTON'
330
+ ? target
331
+ : this.findAncestor(e.target, 'button');
332
+ this.buttonRef.current = button;
333
+ };
334
+
335
+ toggleMenu = (e, selector) => {
336
+ if (this.state.showMenu) {
337
+ this.closeMenu();
338
+ return;
339
+ }
340
+ // PersonalTools always shows at bottom
341
+ if (selector === 'personalTools') {
342
+ this.setState(
343
+ (state) => ({
344
+ showMenu: !state.showMenu,
345
+ menuStyle: { bottom: 0 },
346
+ }),
347
+ () => {
348
+ // Scoped only to personalTools — does not affect other toolbar flows
349
+ const candidates =
350
+ this.toolbarWindow.current?.querySelectorAll(
351
+ 'a, button, input, [tabindex]:not([tabindex="-1"])',
352
+ ) ?? [];
353
+ const firstVisible = Array.from(candidates).find((el) => {
354
+ const style = window.getComputedStyle(el);
355
+ return style.display !== 'none' && style.visibility !== 'hidden';
356
+ });
357
+ firstVisible?.focus();
358
+
359
+ // Announce to screen readers: menu opened + which element received focus
360
+ if (this.announceRef.current) {
361
+ const focusedLabel =
362
+ firstVisible?.getAttribute('aria-label') ||
363
+ firstVisible?.textContent?.trim() ||
364
+ '';
365
+ this.announceRef.current.textContent = '';
366
+ setTimeout(() => {
367
+ if (this.announceRef.current) {
368
+ this.announceRef.current.textContent = focusedLabel
369
+ ? `${this.props.intl.formatMessage(messages.menuOpened)}, ${this.props.intl.formatMessage(messages.focusOn)} ${focusedLabel}`
370
+ : this.props.intl.formatMessage(messages.menuOpened);
371
+ }
372
+ }, 100);
373
+ }
374
+ },
375
+ );
376
+ } else if (selector === 'more') {
377
+ this.setState((state) => ({
378
+ showMenu: !state.showMenu,
379
+ menuStyle: {
380
+ overflow: 'visible',
381
+ top: 0,
382
+ },
383
+ }));
384
+ } else {
385
+ this.setState((state) => ({
386
+ showMenu: !state.showMenu,
387
+ menuStyle: { top: 0 },
388
+ }));
389
+ }
390
+ this.toggleButtonPressed(e);
391
+ this.loadComponent(selector);
392
+ };
393
+
394
+ findAncestor = (el, sel) => {
395
+ while (
396
+ (el = el.parentElement) &&
397
+ !(el.matches || el.matchesSelector).call(el, sel)
398
+ );
399
+ return el;
400
+ };
401
+
402
+ handleClickOutside = (e) => {
403
+ const target = e.target;
404
+
405
+ if (this.pusher && doesNodeContainClick(this.pusher, e)) {
406
+ return;
407
+ }
408
+
409
+ if (
410
+ this.toolbarRef.current &&
411
+ doesNodeContainClick(this.toolbarRef.current, e)
412
+ ) {
413
+ return;
414
+ }
415
+
416
+ if (target.closest('.ui.modal') || target.closest('.ui.dimmer')) {
417
+ return;
418
+ }
419
+
420
+ const button =
421
+ doesNodeContainClick(this.toolbarRef.current, e) &&
422
+ this.findAncestor(target, 'button');
423
+ if (button && button === this.buttonRef.current) {
424
+ return;
425
+ }
426
+
427
+ this.closeMenu();
428
+ };
429
+
430
+ unlock = (e) => {
431
+ this.props.unlockContent(getBaseUrl(this.props.pathname), true);
432
+ };
433
+
434
+ /**
435
+ * Render method.
436
+ * @method render
437
+ * @returns {string} Markup for the component.
438
+ */
439
+ render() {
440
+ const path = getBaseUrl(this.props.pathname);
441
+ const lock = this.props.content?.lock;
442
+ const unlockAction =
443
+ lock?.locked && lock?.stealable && lock?.creator !== this.props.userId;
444
+ const editAction =
445
+ !unlockAction && find(this.props.actions.object, { id: 'edit' });
446
+ const folderContentsAction = find(this.props.actions.object, {
447
+ id: 'folderContents',
448
+ });
449
+ const { expanded } = this.state;
450
+
451
+ return (
452
+ this.props.token && (
453
+ <>
454
+ <BodyClass
455
+ className={expanded ? 'has-toolbar' : 'has-toolbar-collapsed'}
456
+ />
457
+ <span
458
+ aria-live="assertive"
459
+ aria-atomic="true"
460
+ role="status"
461
+ className="visually-hidden"
462
+ ref={this.announceRef}
463
+ />
464
+ <div
465
+ style={this.state.menuStyle}
466
+ className={
467
+ this.state.showMenu ? 'toolbar-content show' : 'toolbar-content'
468
+ }
469
+ ref={this.toolbarWindow}
470
+ onBlur={(e) => {
471
+ if (
472
+ e.relatedTarget &&
473
+ !this.toolbarWindow.current?.contains(e.relatedTarget)
474
+ ) {
475
+ this.toolbarRef.current
476
+ ?.querySelector('button.toolbar-handler-button')
477
+ ?.focus();
478
+
479
+ this.closeMenu();
480
+
481
+ if (this.announceRef.current) {
482
+ this.announceRef.current.textContent = '';
483
+ // Timeout to allow the screen reader to pick up the change in content after the menu is closed
484
+ setTimeout(() => {
485
+ if (this.announceRef.current) {
486
+ this.announceRef.current.textContent =
487
+ this.props.intl.formatMessage(messages.menuClosed);
488
+ }
489
+ }, 100);
490
+ }
491
+ }
492
+ }}
493
+ >
494
+ {this.state.showMenu && (
495
+ // This sets the scroll locker in the body tag in mobile
496
+ <BodyClass className="has-toolbar-menu-open" />
497
+ )}
498
+ <div
499
+ className="pusher-puller"
500
+ ref={(node) => (this.pusher = node)}
501
+ style={{
502
+ transform: this.toolbarWindow.current
503
+ ? `translateX(-${
504
+ (this.state.loadedComponents.length - 1) *
505
+ this.toolbarWindow.current.getBoundingClientRect().width
506
+ }px)`
507
+ : null,
508
+ }}
509
+ >
510
+ {this.state.loadedComponents.map((component, index) =>
511
+ (() => {
512
+ const ToolbarComponent =
513
+ toolbarComponents[component].component;
514
+ const WrapperComponent = toolbarComponents[component].wrapper;
515
+ const haveActions =
516
+ toolbarComponents[component].hideToolbarBody;
517
+ const title =
518
+ toolbarComponents[component].wrapperTitle &&
519
+ this.props.intl.formatMessage(
520
+ toolbarComponents[component].wrapperTitle,
521
+ );
522
+ if (WrapperComponent) {
523
+ return (
524
+ <WrapperComponent
525
+ componentName={component}
526
+ componentTitle={title}
527
+ pathname={this.props.pathname}
528
+ loadComponent={this.loadComponent}
529
+ unloadComponent={this.unloadComponent}
530
+ componentIndex={index}
531
+ theToolbar={this.toolbarWindow}
532
+ key={`personalToolsComponent-${index}`}
533
+ closeMenu={this.closeMenu}
534
+ hasActions={haveActions}
535
+ >
536
+ <ToolbarComponent
537
+ pathname={this.props.pathname}
538
+ loadComponent={this.loadComponent}
539
+ unloadComponent={this.unloadComponent}
540
+ componentIndex={index}
541
+ theToolbar={this.toolbarWindow}
542
+ closeMenu={this.closeMenu}
543
+ isToolbarEmbedded
544
+ />
545
+ </WrapperComponent>
546
+ );
547
+ } else {
548
+ return (
549
+ <ToolbarComponent
550
+ pathname={this.props.pathname}
551
+ loadComponent={this.loadComponent}
552
+ unloadComponent={this.unloadComponent}
553
+ componentIndex={index}
554
+ theToolbar={this.toolbarWindow}
555
+ key={`personalToolsComponent-${index}`}
556
+ closeMenu={this.closeMenu}
557
+ content={
558
+ toolbarComponents[component].contentAsProps
559
+ ? this.props.content
560
+ : null
561
+ }
562
+ />
563
+ );
564
+ }
565
+ })(),
566
+ )}
567
+ </div>
568
+ </div>
569
+ <div
570
+ id="toolbar-body"
571
+ className={this.state.expanded ? 'toolbar expanded' : 'toolbar'}
572
+ ref={this.toolbarRef}
573
+ >
574
+ <div className="toolbar-body">
575
+ <div className="toolbar-actions">
576
+ {this.props.hideDefaultViewButtons && this.props.inner && (
577
+ <>{this.props.inner}</>
578
+ )}
579
+ {!this.props.hideDefaultViewButtons && (
580
+ <>
581
+ {unlockAction && (
582
+ <button
583
+ aria-label={this.props.intl.formatMessage(
584
+ messages.unlock,
585
+ )}
586
+ className="unlock"
587
+ onClick={(e) => this.unlock(e)}
588
+ tabIndex={0}
589
+ >
590
+ <Icon
591
+ name={unlockSVG}
592
+ size="30px"
593
+ className="unlock"
594
+ title={this.props.intl.formatMessage(messages.unlock)}
595
+ />
596
+ </button>
597
+ )}
598
+
599
+ {editAction && (
600
+ <Link
601
+ aria-label={this.props.intl.formatMessage(
602
+ messages.edit,
603
+ )}
604
+ className="edit"
605
+ to={`${path}/edit`}
606
+ >
607
+ <Icon
608
+ name={penSVG}
609
+ size="30px"
610
+ className="circled"
611
+ title={this.props.intl.formatMessage(messages.edit)}
612
+ />
613
+ </Link>
614
+ )}
615
+ {this.props.content &&
616
+ this.props.content.is_folderish &&
617
+ folderContentsAction &&
618
+ !this.props.pathname.endsWith('/contents') && (
619
+ <Link
620
+ aria-label={this.props.intl.formatMessage(
621
+ messages.contents,
622
+ )}
623
+ to={`${path}/contents`}
624
+ >
625
+ <Icon
626
+ name={folderSVG}
627
+ size="30px"
628
+ title={this.props.intl.formatMessage(
629
+ messages.contents,
630
+ )}
631
+ />
632
+ </Link>
633
+ )}
634
+ {this.props.content &&
635
+ this.props.content.is_folderish &&
636
+ folderContentsAction &&
637
+ this.props.pathname.endsWith('/contents') && (
638
+ <Link
639
+ to={`${path}`}
640
+ aria-label={this.props.intl.formatMessage(
641
+ messages.back,
642
+ )}
643
+ >
644
+ <Icon
645
+ name={backSVG}
646
+ className="circled"
647
+ size="30px"
648
+ title={this.props.intl.formatMessage(messages.back)}
649
+ />
650
+ </Link>
651
+ )}
652
+ {this.props.content &&
653
+ this.props.content.is_folderish &&
654
+ (this.props.types.length > 0 ||
655
+ this.props.content['@components']?.translations) && (
656
+ <button
657
+ className="add"
658
+ aria-label={this.props.intl.formatMessage(
659
+ messages.add,
660
+ )}
661
+ onClick={(e) => this.toggleMenu(e, 'types')}
662
+ tabIndex={0}
663
+ id="toolbar-add"
664
+ >
665
+ <Icon
666
+ name={addSVG}
667
+ size="30px"
668
+ title={this.props.intl.formatMessage(messages.add)}
669
+ />
670
+ </button>
671
+ )}
672
+ <div className="toolbar-button-spacer" />
673
+ <button
674
+ className="more"
675
+ aria-label={this.props.intl.formatMessage(messages.more)}
676
+ onClick={(e) => this.toggleMenu(e, 'more')}
677
+ tabIndex={0}
678
+ id="toolbar-more"
679
+ >
680
+ <Icon
681
+ className="mobile hidden"
682
+ name={moreSVG}
683
+ size="30px"
684
+ title={this.props.intl.formatMessage(messages.more)}
685
+ />
686
+ {this.state.showMenu ? (
687
+ <Icon
688
+ className="mobile only"
689
+ name={clearSVG}
690
+ size="30px"
691
+ />
692
+ ) : (
693
+ <Icon
694
+ className="mobile only"
695
+ name={moreSVG}
696
+ size="30px"
697
+ />
698
+ )}
699
+ </button>
700
+ </>
701
+ )}
702
+ <Pluggable name="main.toolbar.top" />
703
+ </div>
704
+ <div className="toolbar-bottom">
705
+ <Pluggable
706
+ name="main.toolbar.bottom"
707
+ params={{ onClickHandler: this.toggleMenu }}
708
+ />
709
+ {!this.props.hideDefaultViewButtons && (
710
+ <button
711
+ className="user"
712
+ aria-label={this.props.intl.formatMessage(
713
+ messages.personalTools,
714
+ )}
715
+ onClick={(e) => this.toggleMenu(e, 'personalTools')}
716
+ tabIndex={0}
717
+ id="toolbar-personal"
718
+ >
719
+ <UserAvatar size="30px" />
720
+ </button>
721
+ )}
722
+ </div>
723
+ </div>
724
+ <div className="toolbar-handler">
725
+ <button
726
+ className={cx('toolbar-handler-button', {
727
+ [this.props.content?.review_state]:
728
+ this.props.content?.review_state,
729
+ })}
730
+ onClick={this.handleShrink}
731
+ aria-expanded={expanded}
732
+ aria-controls="toolbar-body"
733
+ >
734
+ <span aria-live="assertive" className="visually-hidden">
735
+ {expanded
736
+ ? this.props.intl.formatMessage(messages.shrinkToolbar)
737
+ : this.props.intl.formatMessage(messages.expandToolbar)}
738
+ </span>
739
+ </button>
740
+ </div>
741
+ </div>
742
+ <div className="pusher" />
743
+ </>
744
+ )
745
+ );
746
+ }
747
+ }
748
+
749
+ export default compose(
750
+ injectIntl,
751
+ withCookies,
752
+ connect(
753
+ (state, props) => ({
754
+ actions: state.actions.actions,
755
+ token: state.userSession.token,
756
+ userId: state.userSession.token
757
+ ? jwtDecode(state.userSession.token).sub
758
+ : '',
759
+ content: state.content.data,
760
+ pathname: props.pathname,
761
+ types: filter(state.types.types, 'addable'),
762
+ unlockRequest: state.content.unlock,
763
+ }),
764
+ { getTypes, listActions, setExpandedToolbar, unlockContent },
765
+ ),
766
+ )(Toolbar);