@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.
- package/.changelog.draft +261 -0
- package/.release-it.json +34 -0
- package/CHANGELOG.md +269 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/babel.config.js +17 -0
- package/locales/de/LC_MESSAGES/volto.po +1162 -0
- package/locales/en/LC_MESSAGES/volto.po +1162 -0
- package/locales/es/LC_MESSAGES/volto.po +1169 -0
- package/locales/pt_BR/LC_MESSAGES/volto.po +1167 -0
- package/locales/volto.pot +1164 -0
- package/news/.changelog_template.jinja +15 -0
- package/news/.gitkeep +0 -0
- package/package.json +66 -0
- package/public/.gitkeep +0 -0
- package/src/actions/actions.test.ts +226 -0
- package/src/actions/index.ts +530 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/Applications/Applications.tsx +169 -0
- package/src/components/Applications/ApplicationsPanel.scss +59 -0
- package/src/components/Applications/ApplicationsPanel.stories.tsx +82 -0
- package/src/components/Applications/ApplicationsPanel.test.tsx +235 -0
- package/src/components/Applications/ApplicationsPanel.tsx +342 -0
- package/src/components/Callback/Callback.scss +17 -0
- package/src/components/Callback/Callback.stories.tsx +43 -0
- package/src/components/Callback/Callback.test.tsx +262 -0
- package/src/components/Callback/Callback.tsx +170 -0
- package/src/components/Consent/Consent.test.tsx +75 -0
- package/src/components/Consent/Consent.tsx +98 -0
- package/src/components/Consent/ConsentPanel.scss +37 -0
- package/src/components/Consent/ConsentPanel.stories.tsx +80 -0
- package/src/components/Consent/ConsentPanel.test.tsx +138 -0
- package/src/components/Consent/ConsentPanel.tsx +162 -0
- package/src/components/ControlPanel/ClientsControlPanel.stories.tsx +38 -0
- package/src/components/ControlPanel/ClientsControlPanel.tsx +321 -0
- package/src/components/ControlPanel/ClientsPanel.scss +21 -0
- package/src/components/ControlPanel/ClientsPanel.stories.tsx +71 -0
- package/src/components/ControlPanel/ClientsPanel.test.tsx +267 -0
- package/src/components/ControlPanel/ClientsPanel.tsx +386 -0
- package/src/components/ControlPanel/ConfirmModal.stories.tsx +35 -0
- package/src/components/ControlPanel/ConfirmModal.test.tsx +66 -0
- package/src/components/ControlPanel/ConfirmModal.tsx +68 -0
- package/src/components/ControlPanel/ProviderForm.stories.tsx +107 -0
- package/src/components/ControlPanel/ProvidersControlPanel.scss +26 -0
- package/src/components/ControlPanel/ProvidersControlPanel.stories.tsx +88 -0
- package/src/components/ControlPanel/ProvidersControlPanel.tsx +600 -0
- package/src/components/ControlPanel/RenderUsers.stories.tsx +112 -0
- package/src/components/ControlPanel/RenderUsers.test.tsx +145 -0
- package/src/components/ControlPanel/RenderUsers.tsx +263 -0
- package/src/components/ControlPanel/SecretReveal.scss +32 -0
- package/src/components/ControlPanel/SecretReveal.stories.tsx +33 -0
- package/src/components/ControlPanel/SecretReveal.test.tsx +101 -0
- package/src/components/ControlPanel/SecretReveal.tsx +105 -0
- package/src/components/ControlPanel/UserAccount.stories.tsx +69 -0
- package/src/components/ControlPanel/UserAccount.test.tsx +117 -0
- package/src/components/ControlPanel/UserAccount.tsx +125 -0
- package/src/components/ControlPanel/UserAccountPanel.scss +38 -0
- package/src/components/ControlPanel/UserAccountPanel.stories.tsx +77 -0
- package/src/components/ControlPanel/UserAccountPanel.test.tsx +173 -0
- package/src/components/ControlPanel/UserAccountPanel.tsx +273 -0
- package/src/components/FirstLogin/FirstLogin.scss +12 -0
- package/src/components/FirstLogin/FirstLogin.stories.tsx +50 -0
- package/src/components/FirstLogin/FirstLogin.tsx +94 -0
- package/src/components/Identities/Identities.stories.tsx +51 -0
- package/src/components/Identities/Identities.test.tsx +134 -0
- package/src/components/Identities/Identities.tsx +217 -0
- package/src/components/Identities/IdentitiesList.scss +49 -0
- package/src/components/Identities/IdentitiesList.stories.tsx +71 -0
- package/src/components/Identities/IdentitiesList.test.tsx +218 -0
- package/src/components/Identities/IdentitiesList.tsx +207 -0
- package/src/components/Identities/ProfileEmails.scss +62 -0
- package/src/components/Identities/ProfileEmails.stories.tsx +75 -0
- package/src/components/Identities/ProfileEmails.test.tsx +132 -0
- package/src/components/Identities/ProfileEmails.tsx +217 -0
- package/src/components/Login/Login.stories.tsx +63 -0
- package/src/components/Login/Login.test.tsx +189 -0
- package/src/components/Login/Login.tsx +163 -0
- package/src/components/Login/LoginForm.scss +39 -0
- package/src/components/Login/LoginForm.stories.tsx +112 -0
- package/src/components/Login/LoginForm.test.tsx +483 -0
- package/src/components/Login/LoginForm.tsx +321 -0
- package/src/components/Login/LoginOverlay.scss +44 -0
- package/src/components/Login/LoginOverlay.stories.tsx +73 -0
- package/src/components/Login/LoginOverlay.test.tsx +98 -0
- package/src/components/Login/LoginOverlay.tsx +107 -0
- package/src/components/Login/LoginPanel.scss +50 -0
- package/src/components/Login/LoginPanel.stories.tsx +40 -0
- package/src/components/Login/LoginPanel.tsx +60 -0
- package/src/components/Login/MagicLinkForm.scss +25 -0
- package/src/components/Login/MagicLinkForm.stories.tsx +27 -0
- package/src/components/Login/MagicLinkForm.tsx +133 -0
- package/src/components/Login/PasswordForm.scss +82 -0
- package/src/components/Login/PasswordForm.stories.tsx +26 -0
- package/src/components/Login/PasswordForm.tsx +159 -0
- package/src/components/Login/ProviderButton.scss +103 -0
- package/src/components/Login/ProviderButton.stories.tsx +74 -0
- package/src/components/Login/ProviderButton.test.tsx +99 -0
- package/src/components/Login/ProviderButton.tsx +97 -0
- package/src/components/ProfileGate/ProfileGate.stories.tsx +118 -0
- package/src/components/ProfileGate/ProfileGate.test.tsx +370 -0
- package/src/components/ProfileGate/ProfileGate.tsx +172 -0
- package/src/components/Toolbar/PersonalTools.stories.tsx +111 -0
- package/src/components/Toolbar/PersonalTools.test.tsx +165 -0
- package/src/components/Toolbar/PersonalTools.tsx +177 -0
- package/src/components/Toolbar/Toolbar.jsx +766 -0
- package/src/components/UserMenu/ApplicationsMenuItem.stories.tsx +62 -0
- package/src/components/UserMenu/ApplicationsMenuItem.test.tsx +109 -0
- package/src/components/UserMenu/ApplicationsMenuItem.tsx +82 -0
- package/src/components/UserMenu/IdentitiesMenuItem.stories.tsx +35 -0
- package/src/components/UserMenu/IdentitiesMenuItem.test.tsx +103 -0
- package/src/components/UserMenu/IdentitiesMenuItem.tsx +34 -0
- package/src/components/UserMenu/MenuItem.stories.tsx +32 -0
- package/src/components/UserMenu/MenuItem.tsx +48 -0
- package/src/components/UserMenu/ProfileMenuItem.stories.tsx +76 -0
- package/src/components/UserMenu/ProfileMenuItem.test.tsx +90 -0
- package/src/components/UserMenu/ProfileMenuItem.tsx +60 -0
- package/src/components/UserMenu/UserAvatar.scss +23 -0
- package/src/components/UserMenu/UserAvatar.stories.tsx +121 -0
- package/src/components/UserMenu/UserAvatar.test.tsx +120 -0
- package/src/components/UserMenu/UserAvatar.tsx +119 -0
- package/src/components/UserMenu/UserMenuPlugs.stories.tsx +101 -0
- package/src/components/UserMenu/UserMenuPlugs.test.tsx +202 -0
- package/src/components/UserMenu/UserMenuPlugs.tsx +142 -0
- package/src/components/UserMenu/UserProfileLoader.test.tsx +211 -0
- package/src/components/UserMenu/UserProfileLoader.tsx +54 -0
- package/src/components/Views/GroupView.scss +36 -0
- package/src/components/Views/GroupView.stories.tsx +98 -0
- package/src/components/Views/GroupView.test.tsx +137 -0
- package/src/components/Views/GroupView.tsx +178 -0
- package/src/components/Views/ProfileView.scss +27 -0
- package/src/components/Views/ProfileView.stories.tsx +34 -0
- package/src/components/Views/ProfileView.test.tsx +82 -0
- package/src/components/Views/ProfileView.tsx +100 -0
- package/src/components/Widgets/ProviderIconWidget.scss +21 -0
- package/src/components/Widgets/ProviderIconWidget.stories.tsx +30 -0
- package/src/components/Widgets/ProviderIconWidget.test.tsx +55 -0
- package/src/components/Widgets/ProviderIconWidget.tsx +143 -0
- package/src/config/menu.ts +35 -0
- package/src/config/reducers.ts +7 -0
- package/src/config/routes.test.ts +177 -0
- package/src/config/routes.ts +143 -0
- package/src/config/settings.test.ts +34 -0
- package/src/config/settings.ts +72 -0
- package/src/config/views.ts +30 -0
- package/src/config/widgets.ts +25 -0
- package/src/constants/ActionTypes.ts +42 -0
- package/src/constants/vocabularies.ts +20 -0
- package/src/customizations/volto/components/manage/Controlpanels/Users/RenderUsers.tsx +23 -0
- package/src/customizations/volto/components/manage/Toolbar/PersonalTools.tsx +21 -0
- package/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx +21 -0
- package/src/helpers/avatar.test.ts +79 -0
- package/src/helpers/avatar.ts +85 -0
- package/src/helpers/callback.test.ts +60 -0
- package/src/helpers/callback.ts +71 -0
- package/src/helpers/clientSchema.test.ts +182 -0
- package/src/helpers/clientSchema.ts +181 -0
- package/src/helpers/firstLogin.test.ts +90 -0
- package/src/helpers/firstLogin.ts +76 -0
- package/src/helpers/groupmap.test.ts +81 -0
- package/src/helpers/groupmap.ts +50 -0
- package/src/helpers/identities.ts +22 -0
- package/src/helpers/navigate.test.ts +132 -0
- package/src/helpers/navigate.ts +126 -0
- package/src/helpers/profileGate.test.ts +150 -0
- package/src/helpers/profileGate.ts +191 -0
- package/src/helpers/profileSource.ts +41 -0
- package/src/helpers/propertymap.test.ts +71 -0
- package/src/helpers/propertymap.ts +43 -0
- package/src/helpers/providerSchema.test.ts +410 -0
- package/src/helpers/providerSchema.ts +362 -0
- package/src/helpers/returnUrl.test.ts +42 -0
- package/src/helpers/returnUrl.ts +41 -0
- package/src/helpers/rowmap.ts +110 -0
- package/src/helpers/showPloneLogin.test.ts +78 -0
- package/src/helpers/showPloneLogin.ts +55 -0
- package/src/helpers/token.test.ts +62 -0
- package/src/helpers/token.ts +71 -0
- package/src/index.ts +22 -0
- package/src/reducers/index.ts +369 -0
- package/src/reducers/reducers.test.ts +369 -0
- package/src/stories/fixtures.tsx +640 -0
- package/src/storybook/withPage.test.tsx +90 -0
- package/src/storybook/withPage.tsx +110 -0
- package/src/storybook/withUserMenu.test.tsx +78 -0
- package/src/storybook/withUserMenu.tsx +136 -0
- package/src/styles.css +262 -0
- package/src/testing.tsx +86 -0
- package/src/types.ts +441 -0
- package/towncrier.toml +38 -0
- package/tsconfig.json +22 -0
- 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;
|