@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,165 @@
1
+ /**
2
+ * The shadowed personal-tools menu.
3
+ *
4
+ * Upstream ships no test for this component, and once it is rewritten rather
5
+ * than patched it is this package's code: the things that differ from Volto
6
+ * are the things worth pinning, so a future upgrade that quietly reverts one
7
+ * of them fails here instead of in somebody's browser.
8
+ */
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import { render, screen } from '../../testing';
11
+ import { MemoryRouter } from 'react-router-dom';
12
+ import { Provider } from 'react-redux';
13
+ import React from 'react';
14
+
15
+ import {
16
+ Plug,
17
+ PluggablesProvider,
18
+ } from '@plone/volto/components/manage/Pluggable';
19
+ import PersonalTools from './PersonalTools';
20
+
21
+ const ALICE = {
22
+ id: 'alice',
23
+ username: 'alice@example.com',
24
+ fullname: 'Alice Liddell',
25
+ };
26
+
27
+ function renderMenu(
28
+ user: unknown = ALICE,
29
+ props: Record<string, unknown> = {},
30
+ actions: unknown[] = [],
31
+ plugs: React.ReactNode = null,
32
+ ) {
33
+ const store = {
34
+ getState: () => ({
35
+ userProfile: { loading: false, loaded: true, error: null, data: user },
36
+ actions: { actions: { user: actions } },
37
+ }),
38
+ dispatch: vi.fn(),
39
+ subscribe: () => () => {},
40
+ };
41
+ const result = render(
42
+ <Provider store={store as never}>
43
+ <MemoryRouter>
44
+ <PluggablesProvider>
45
+ {plugs}
46
+ <PersonalTools
47
+ loadComponent={vi.fn()}
48
+ unloadComponent={vi.fn()}
49
+ theToolbar={{ current: null }}
50
+ {...props}
51
+ />
52
+ </PluggablesProvider>
53
+ </MemoryRouter>
54
+ </Provider>,
55
+ );
56
+ return { ...result, store };
57
+ }
58
+
59
+ describe('PersonalTools', () => {
60
+ it('writes no entry of its own into the list', () => {
61
+ // Upstream writes three and puts the pluggable after them, so an add-on
62
+ // can only append. Every entry is a plug now -- what they are and what
63
+ // order they come in is `UserMenuPlugs`' test -- and this component's
64
+ // share of it is an empty list waiting for them.
65
+ const { container } = renderMenu();
66
+
67
+ expect(container.querySelector('.pastanaga-menu-list ul')).toBeTruthy();
68
+ expect(container.querySelectorAll('.pastanaga-menu-list li')).toHaveLength(
69
+ 0,
70
+ );
71
+ });
72
+
73
+ it('renders whatever plugged into the user menu', () => {
74
+ renderMenu(
75
+ ALICE,
76
+ {},
77
+ [],
78
+ <Plug pluggable="toolbar-user-menu" id="probe">
79
+ <li id="probe-entry">Probe</li>
80
+ </Plug>,
81
+ );
82
+
83
+ expect(document.querySelector('#probe-entry')).toBeTruthy();
84
+ });
85
+
86
+ it('hands the plugs the prop only it has', () => {
87
+ // `loadComponent` belongs to this component; the Preferences entry is a
88
+ // plug and would otherwise have no way to reach it.
89
+ const loadComponent = vi.fn();
90
+ renderMenu(
91
+ ALICE,
92
+ { loadComponent },
93
+ [],
94
+ <Plug pluggable="toolbar-user-menu" id="probe">
95
+ {({ loadComponent: given }: { loadComponent: () => void }) => (
96
+ <li>
97
+ <button onClick={() => given()}>Probe</button>
98
+ </li>
99
+ )}
100
+ </Plug>,
101
+ );
102
+
103
+ screen.getByRole('button', { name: 'Probe' }).click();
104
+
105
+ expect(loadComponent).toHaveBeenCalled();
106
+ });
107
+
108
+ it('names the user from the store this package fills', () => {
109
+ renderMenu();
110
+
111
+ expect(screen.getByRole('heading').textContent).toBe('Alice Liddell');
112
+ });
113
+
114
+ it('never leaves the header empty', () => {
115
+ // A blank header reads as a broken menu rather than as a user who has
116
+ // not filled their name in.
117
+ renderMenu({ id: 'alice', username: 'alice@example.com' });
118
+ expect(screen.getByRole('heading').textContent).toBe('alice@example.com');
119
+
120
+ screen.getByRole('heading').remove();
121
+ renderMenu({ id: 'alice' });
122
+ expect(screen.getByRole('heading').textContent).toBe('alice');
123
+ });
124
+
125
+ it('does not fetch the user itself', () => {
126
+ // Upstream decodes the JWT and dispatches getUser on every mount. This
127
+ // add-on already loaded the same `@users/<userid>` once, per userid.
128
+ const { store } = renderMenu();
129
+
130
+ expect(store.dispatch).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it('slides out through the prop it was given', () => {
134
+ const unloadComponent = vi.fn();
135
+ renderMenu(ALICE, { unloadComponent });
136
+
137
+ screen.getByRole('button', { name: /back/i }).click();
138
+
139
+ expect(unloadComponent).toHaveBeenCalled();
140
+ });
141
+
142
+ it('names the back button and the logout link', () => {
143
+ // Upstream leaves both unnamed: an `<Icon>` with a `title` inside an
144
+ // element with no label of its own is announced as "button".
145
+ renderMenu();
146
+
147
+ expect(screen.getByRole('button', { name: /back/i })).toBeTruthy();
148
+ expect(screen.getByRole('link', { name: /logout/i })).toBeTruthy();
149
+ });
150
+
151
+ it('renders no avatar block', () => {
152
+ // It moved to the toolbar button that opens this menu.
153
+ const { container } = renderMenu();
154
+
155
+ expect(container.querySelector('.avatar')).toBeNull();
156
+ });
157
+
158
+ it('keeps the class names the stylesheet is written against', () => {
159
+ const { container } = renderMenu(ALICE, { hasActions: true });
160
+
161
+ const root = container.querySelector('.personal-tools');
162
+ expect(root?.className).toContain('pastanaga-menu');
163
+ expect(root?.className).toContain('has-inner-actions');
164
+ });
165
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The personal-tools menu, rewritten.
3
+ *
4
+ * Takes Volto's place through
5
+ * `customizations/volto/components/manage/Toolbar/PersonalTools`, which is
6
+ * where the reason for shadowing it is written down.
7
+ *
8
+ * **Not** a verbatim copy of `@plone/volto/components/manage/Toolbar/PersonalTools`
9
+ * (Volto 19.3.0) with a patch: it is a rewrite in TypeScript that keeps the
10
+ * markup, the class names and the behaviour Volto's stylesheet and tests
11
+ * expect, and changes what was worth changing. On a Volto upgrade, read
12
+ * upstream's diff for *behavioural* changes and port those -- a literal file
13
+ * diff will not be meaningful.
14
+ *
15
+ * WHAT DIFFERS FROM UPSTREAM, and why:
16
+ *
17
+ * - **The menu list is nothing but the pluggable.** Upstream writes Profile,
18
+ * Preferences and Site Setup into the list and puts `toolbar-user-menu`
19
+ * after them, so an add-on can only append: wanting an entry *between* two
20
+ * of Volto's, or wanting one of them gone, meant shadowing this component
21
+ * again. All three are plugs now -- see
22
+ * `components/UserMenu/UserMenuPlugs` -- so the menu is one ordered list a
23
+ * site composes. `loadComponent` reaches the Preferences entry through the
24
+ * pluggable's `params`, which is what that mechanism is for.
25
+ *
26
+ * - **The avatar block is gone.** It drew a 96px portrait, or a camera icon
27
+ * for the many users who have never uploaded one -- the same picture for
28
+ * everybody, saying "no image here" rather than "this is you". Who is
29
+ * signed in is shown by `<UserAvatar>` on the toolbar button that opens
30
+ * this menu, where it is visible without opening anything.
31
+ *
32
+ * - **The Profile link has a stable id.** Upstream sets
33
+ * `id={intl.formatMessage(messages.profile)}`, so the DOM id is the
34
+ * *translated* label: `id="Profile"` in English, `id="Perfil"` in
35
+ * Portuguese. Anything keying on it -- a test, a stylesheet, an
36
+ * integration -- works in one language and silently not in the next. It is
37
+ * `id="toolbar-profile"` in the plug that now renders it, matching the
38
+ * `toolbar-logout` in the header here, which upstream already spells that
39
+ * way.
40
+ *
41
+ * - **It does not fetch the user.** Upstream decodes the JWT and dispatches
42
+ * `getUser` on mount. This add-on already holds the signed-in user, loaded
43
+ * once per userid by `UserProfileLoader` from the same `@users/<userid>`
44
+ * endpoint, so fetching again on every menu open is a duplicate request for
45
+ * data already in the store. `jwt-decode` goes with it.
46
+ *
47
+ * - **The back button and the logout link have accessible names.** Both were
48
+ * an `<Icon>` with a `title` inside an element with no label of its own, so
49
+ * a screen reader announced them as an unnamed button and an unnamed link.
50
+ * The Preferences button beside them already carried an `aria-label`; these
51
+ * two now do too.
52
+ *
53
+ * - **No `classnames`.** Upstream uses it for one conditional class. It is
54
+ * Volto's dependency rather than this package's, and this add-on declares
55
+ * no runtime dependencies at all -- so the one class is joined by hand.
56
+ *
57
+ * - **Dead code is gone.** `const [, setPushed] = useState(false)` -- the
58
+ * value was never read, so the only effect of setting it was a re-render
59
+ * per menu push. The commented-out `<Stats />` and the "should be a
60
+ * Component by itself" note went too.
61
+ *
62
+ * @module components/Toolbar/PersonalTools
63
+ */
64
+ import React from 'react';
65
+ import type { RefObject } from 'react';
66
+ import { useSelector } from 'react-redux';
67
+ import { Link, useLocation } from 'react-router-dom';
68
+ import { useIntl, defineMessages } from 'react-intl';
69
+
70
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
71
+ import { Pluggable } from '@plone/volto/components/manage/Pluggable';
72
+ import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
73
+ import logoutSVG from '@plone/volto/icons/log-out.svg';
74
+ import backSVG from '@plone/volto/icons/back.svg';
75
+
76
+ const messages = defineMessages({
77
+ back: { id: 'Back', defaultMessage: 'Back' },
78
+ logout: { id: 'Logout', defaultMessage: 'Logout' },
79
+ });
80
+
81
+ export interface PersonalToolsProps {
82
+ /** Slide another panel into the toolbar, by its registered name. */
83
+ loadComponent: (selector: string) => void;
84
+ /** Slide this panel back out. */
85
+ unloadComponent: () => void;
86
+ /** The toolbar element, read for the width this panel matches. */
87
+ theToolbar: RefObject<HTMLElement | null>;
88
+ /** Whether the toolbar is showing content actions beside this menu. */
89
+ hasActions?: boolean;
90
+ }
91
+
92
+ /**
93
+ * The name to show for the signed-in user.
94
+ *
95
+ * Never empty: a header with nothing in it reads as a broken menu rather
96
+ * than as a user who has not filled their name in.
97
+ *
98
+ * @param user The user held in the store, if loaded.
99
+ * @returns The full name, the login name, or the userid.
100
+ */
101
+ function displayName(user: {
102
+ fullname?: string | null;
103
+ username?: string | null;
104
+ id?: string | null;
105
+ }): string {
106
+ return user?.fullname || user?.username || user?.id || '';
107
+ }
108
+
109
+ const PersonalTools: React.FC<PersonalToolsProps> = ({
110
+ loadComponent,
111
+ unloadComponent,
112
+ theToolbar,
113
+ hasActions,
114
+ }) => {
115
+ const intl = useIntl();
116
+ const { pathname } = useLocation();
117
+ const user = useSelector((state: any) => state.userProfile?.data) ?? {};
118
+ // Memoized because `Pluggable` memoizes its render on the params object: a
119
+ // fresh one every render would rebuild every entry on every render.
120
+ const pluggableParams = React.useMemo(
121
+ () => ({ loadComponent }),
122
+ [loadComponent],
123
+ );
124
+
125
+ return (
126
+ <div
127
+ className={
128
+ hasActions
129
+ ? 'personal-tools pastanaga-menu has-inner-actions'
130
+ : 'personal-tools pastanaga-menu'
131
+ }
132
+ style={{
133
+ // Read during render, as upstream does. The panel slides in over the
134
+ // toolbar and has to be exactly its width; a layout effect would
135
+ // size it one frame late, which is visible as a jump.
136
+ flex: theToolbar?.current
137
+ ? `0 0 ${theToolbar.current.getBoundingClientRect().width}px`
138
+ : undefined,
139
+ }}
140
+ >
141
+ <header className="header">
142
+ <button
143
+ className="back"
144
+ aria-label={intl.formatMessage(messages.back)}
145
+ onClick={unloadComponent}
146
+ >
147
+ <Icon
148
+ name={backSVG}
149
+ size="30px"
150
+ title={intl.formatMessage(messages.back)}
151
+ />
152
+ </button>
153
+ <div className="vertical divider" />
154
+ <h2>{displayName(user)}</h2>
155
+ <Link
156
+ id="toolbar-logout"
157
+ aria-label={intl.formatMessage(messages.logout)}
158
+ to={`${getBaseUrl(pathname)}/logout`}
159
+ >
160
+ <Icon
161
+ className="logout"
162
+ name={logoutSVG}
163
+ size="30px"
164
+ title={intl.formatMessage(messages.logout)}
165
+ />
166
+ </Link>
167
+ </header>
168
+ <div className="pastanaga-menu-list">
169
+ <ul>
170
+ <Pluggable name="toolbar-user-menu" params={pluggableParams} />
171
+ </ul>
172
+ </div>
173
+ </div>
174
+ );
175
+ };
176
+
177
+ export default PersonalTools;