@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,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import ProviderButton from './ProviderButton';
|
|
4
|
+
import { GITHUB, GOOGLE, KEYCLOAK, STYLED } from '../../stories/fixtures';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ProviderButton> = {
|
|
7
|
+
title: 'Identity/Login/ProviderButton',
|
|
8
|
+
component: ProviderButton,
|
|
9
|
+
args: {
|
|
10
|
+
id: GOOGLE.id,
|
|
11
|
+
driver: GOOGLE.driver,
|
|
12
|
+
label: GOOGLE.title,
|
|
13
|
+
disabled: false,
|
|
14
|
+
onSelect: () => {},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
type Story = StoryObj<typeof ProviderButton>;
|
|
20
|
+
|
|
21
|
+
export const Google: Story = {};
|
|
22
|
+
|
|
23
|
+
export const GitHub: Story = {
|
|
24
|
+
args: { id: GITHUB.id, driver: GITHUB.driver, label: GITHUB.title },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Any provider reached through discovery, which is most of them. */
|
|
28
|
+
export const GenericOIDC: Story = {
|
|
29
|
+
args: { id: KEYCLOAK.id, driver: KEYCLOAK.driver, label: KEYCLOAK.title },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The local password form, which the login page offers as one of these. Its
|
|
34
|
+
* colours are `volto-authomatic`'s `plone` provider.
|
|
35
|
+
*/
|
|
36
|
+
export const Password: Story = {
|
|
37
|
+
args: { driver: 'plone', label: 'Sign in with a password', id: undefined },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Disabled while a redirect is already under way: a second click would start
|
|
42
|
+
* a second flow whose state replaces the first one's.
|
|
43
|
+
*/
|
|
44
|
+
export const Redirecting: Story = { args: { disabled: true } };
|
|
45
|
+
|
|
46
|
+
/** A provider whose title the operator never set falls back to its id. */
|
|
47
|
+
export const Untitled: Story = { args: { label: GOOGLE.id } };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A provider wearing the look an operator gave it in the control panel.
|
|
51
|
+
*
|
|
52
|
+
* The icon is inlined rather than served as an image, which is what lets a
|
|
53
|
+
* monochrome one take the button's own text colour.
|
|
54
|
+
*/
|
|
55
|
+
export const Styled: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
id: STYLED.id,
|
|
58
|
+
driver: STYLED.driver,
|
|
59
|
+
label: STYLED.title,
|
|
60
|
+
icon: STYLED.icon,
|
|
61
|
+
background_color: STYLED.background_color,
|
|
62
|
+
foreground_color: STYLED.foreground_color,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** An icon and no colours: the theme's own button, with a mark on it. */
|
|
67
|
+
export const IconOnly: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
id: STYLED.id,
|
|
70
|
+
driver: STYLED.driver,
|
|
71
|
+
label: STYLED.title,
|
|
72
|
+
icon: STYLED.icon,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { fireEvent, render, screen } from '../../testing';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import ProviderButton from './ProviderButton';
|
|
6
|
+
|
|
7
|
+
const ICON =
|
|
8
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' +
|
|
9
|
+
'<path d="M8 1l7 13H1z"/></svg>';
|
|
10
|
+
|
|
11
|
+
function renderButton(
|
|
12
|
+
props: Partial<React.ComponentProps<typeof ProviderButton>> = {},
|
|
13
|
+
) {
|
|
14
|
+
const onSelect = vi.fn();
|
|
15
|
+
render(
|
|
16
|
+
<ProviderButton
|
|
17
|
+
id="acme"
|
|
18
|
+
driver="oidc-generic"
|
|
19
|
+
label="Acme SSO"
|
|
20
|
+
onSelect={onSelect}
|
|
21
|
+
{...props}
|
|
22
|
+
/>,
|
|
23
|
+
);
|
|
24
|
+
return { onSelect };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('ProviderButton', () => {
|
|
28
|
+
it('wears the driver class when the provider has no look of its own', () => {
|
|
29
|
+
renderButton();
|
|
30
|
+
|
|
31
|
+
const button = screen.getByRole('button');
|
|
32
|
+
expect(button.className).toContain('identity-provider--oidc-generic');
|
|
33
|
+
expect(button.className).not.toContain('identity-provider--styled');
|
|
34
|
+
expect(button.getAttribute('style')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reports the press', () => {
|
|
38
|
+
const { onSelect } = renderButton();
|
|
39
|
+
|
|
40
|
+
fireEvent.click(screen.getByRole('button'));
|
|
41
|
+
|
|
42
|
+
expect(onSelect).toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('ProviderButton and the look an operator set', () => {
|
|
47
|
+
it('wears the colours as an inline style', () => {
|
|
48
|
+
renderButton({ background_color: '#4b3f72', foreground_color: '#ffffff' });
|
|
49
|
+
|
|
50
|
+
const button = screen.getByRole('button') as HTMLButtonElement;
|
|
51
|
+
expect(button.style.backgroundColor).toBeTruthy();
|
|
52
|
+
expect(button.style.color).toBeTruthy();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('marks itself so the stylesheet stands back', () => {
|
|
56
|
+
// Otherwise the driver rules above would fight the inline style on
|
|
57
|
+
// specificity, and which one won would depend on the driver.
|
|
58
|
+
renderButton({ background_color: '#4b3f72' });
|
|
59
|
+
|
|
60
|
+
expect(screen.getByRole('button').className).toContain(
|
|
61
|
+
'identity-provider--styled',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('writes no style at all when neither colour is set', () => {
|
|
66
|
+
// An empty string in a style object produces `background-color: ` in the
|
|
67
|
+
// attribute, which some engines treat as a reset rather than as nothing.
|
|
68
|
+
renderButton({ background_color: '', foreground_color: '' });
|
|
69
|
+
|
|
70
|
+
expect(screen.getByRole('button').getAttribute('style')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('inlines the icon so it can take the button colour', () => {
|
|
74
|
+
renderButton({ icon: ICON });
|
|
75
|
+
|
|
76
|
+
expect(document.querySelector('.identity-provider__icon svg')).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the label beside the icon', () => {
|
|
80
|
+
renderButton({ icon: ICON });
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText('Acme SSO')).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('hides the icon from assistive technology', () => {
|
|
86
|
+
// The label says what the button does; the icon repeating it would be
|
|
87
|
+
// read out twice.
|
|
88
|
+
renderButton({ icon: ICON });
|
|
89
|
+
|
|
90
|
+
const icon = document.querySelector('.identity-provider__icon');
|
|
91
|
+
expect(icon?.getAttribute('aria-hidden')).toBe('true');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders no icon element when there is no icon', () => {
|
|
95
|
+
renderButton();
|
|
96
|
+
|
|
97
|
+
expect(document.querySelector('.identity-provider__icon')).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One button in the list of ways in.
|
|
3
|
+
*
|
|
4
|
+
* Presentational, and deliberately not tied to a `LoginProvider`: the local
|
|
5
|
+
* password is offered as one of these too, wearing `volto-authomatic`'s Plone
|
|
6
|
+
* colours, and it is not a provider the backend knows anything about. The
|
|
7
|
+
* caller supplies the label, the driver whose colours to wear, and what
|
|
8
|
+
* pressing it does.
|
|
9
|
+
*
|
|
10
|
+
* A provider may also carry its *own* look -- an icon and two colours an
|
|
11
|
+
* operator set in the control panel. When it does, that wins over the
|
|
12
|
+
* driver's stylesheet class, because the operator has said what this button
|
|
13
|
+
* should look like and the class is only a default for the ones who have not.
|
|
14
|
+
* @module components/Login/ProviderButton
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
|
|
18
|
+
import type { ProviderStyle } from '../../types';
|
|
19
|
+
|
|
20
|
+
import './ProviderButton.scss';
|
|
21
|
+
|
|
22
|
+
interface ProviderButtonProps extends ProviderStyle {
|
|
23
|
+
/** Whose colours to wear. `plone` is the local password form. */
|
|
24
|
+
driver: string;
|
|
25
|
+
/**
|
|
26
|
+
* What the button says.
|
|
27
|
+
*
|
|
28
|
+
* A provider's own title, which an operator sets and which is therefore not
|
|
29
|
+
* ours to translate; the caller translates its own labels.
|
|
30
|
+
*/
|
|
31
|
+
label: string;
|
|
32
|
+
/** The provider id, for anything selecting this button by it. */
|
|
33
|
+
id?: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
onSelect: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ProviderButton: React.FC<ProviderButtonProps> = ({
|
|
39
|
+
driver,
|
|
40
|
+
label,
|
|
41
|
+
id,
|
|
42
|
+
icon,
|
|
43
|
+
background_color: background,
|
|
44
|
+
foreground_color: foreground,
|
|
45
|
+
disabled = false,
|
|
46
|
+
onSelect,
|
|
47
|
+
}) => {
|
|
48
|
+
// Only the colours that were set. Writing `undefined` into a style object
|
|
49
|
+
// is harmless, but writing an empty string is not: it produces
|
|
50
|
+
// `background-color: ` in the attribute, which some engines treat as a
|
|
51
|
+
// reset of the inherited value rather than as nothing at all.
|
|
52
|
+
const style: React.CSSProperties = {};
|
|
53
|
+
if (background) {
|
|
54
|
+
style.backgroundColor = background;
|
|
55
|
+
}
|
|
56
|
+
if (foreground) {
|
|
57
|
+
style.color = foreground;
|
|
58
|
+
}
|
|
59
|
+
const styled = Boolean(background || foreground);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
className={[
|
|
65
|
+
'identity-provider',
|
|
66
|
+
`identity-provider--${driver}`,
|
|
67
|
+
// Marks the button as carrying its own colours, so the stylesheet can
|
|
68
|
+
// stand back rather than fight an inline style with specificity.
|
|
69
|
+
styled ? 'identity-provider--styled' : '',
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(' ')}
|
|
73
|
+
style={styled ? style : undefined}
|
|
74
|
+
data-provider={id}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
onClick={onSelect}
|
|
77
|
+
>
|
|
78
|
+
{icon ? (
|
|
79
|
+
<span
|
|
80
|
+
className="identity-provider__icon"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
// The source is an SVG document the backend sanitized as it was
|
|
83
|
+
// stored: an allowlist of elements and attributes, no attribute
|
|
84
|
+
// that references a URL, and the whole thing serialized from the
|
|
85
|
+
// parsed tree rather than sliced out of what was pasted. Inlining
|
|
86
|
+
// it rather than using an `<img>` is what lets it inherit the
|
|
87
|
+
// button's colour, which is the entire reason an operator uploads
|
|
88
|
+
// one.
|
|
89
|
+
dangerouslySetInnerHTML={{ __html: icon }}
|
|
90
|
+
/>
|
|
91
|
+
) : null}
|
|
92
|
+
<span className="identity-provider__label">{label}</span>
|
|
93
|
+
</button>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default ProviderButton;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate renders nothing, so the story renders where it sent you.
|
|
3
|
+
*
|
|
4
|
+
* A component whose entire output is a redirect has no picture of its own.
|
|
5
|
+
* What is worth seeing is the decision, so each story mounts the gate on a
|
|
6
|
+
* route and shows the path the app ends up on -- which is the thing that goes
|
|
7
|
+
* wrong, in both directions: a user let through with an unfinished profile,
|
|
8
|
+
* or a user held on a page they can never leave.
|
|
9
|
+
* @module components/ProfileGate/ProfileGate.stories
|
|
10
|
+
*/
|
|
11
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { MemoryRouter, useLocation } from 'react-router-dom';
|
|
14
|
+
|
|
15
|
+
import ProfileGate from './ProfileGate';
|
|
16
|
+
import { LOADED, LOADING, withStore } from '../../stories/fixtures';
|
|
17
|
+
|
|
18
|
+
const PROFILE = 'https://example.org/identity-profiles/alice';
|
|
19
|
+
|
|
20
|
+
/** Show the path the router is on, after the gate has had its say. */
|
|
21
|
+
const Where: React.FC = () => {
|
|
22
|
+
const location = useLocation();
|
|
23
|
+
return (
|
|
24
|
+
<p style={{ fontFamily: 'monospace' }}>
|
|
25
|
+
<strong>Now at:</strong> {location.pathname}
|
|
26
|
+
</p>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Mount the gate on a starting route and report where it left the app. */
|
|
31
|
+
function at(pathname: string) {
|
|
32
|
+
return (Story: React.ComponentType) => (
|
|
33
|
+
<MemoryRouter initialEntries={[pathname]}>
|
|
34
|
+
<Story />
|
|
35
|
+
<Where />
|
|
36
|
+
</MemoryRouter>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function profileState(review_state: string | null, profile: string | null) {
|
|
41
|
+
return {
|
|
42
|
+
...LOADED,
|
|
43
|
+
data: {
|
|
44
|
+
'@id': '/@my-profile',
|
|
45
|
+
userid: 'alice',
|
|
46
|
+
profile,
|
|
47
|
+
review_state,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const meta: Meta<typeof ProfileGate> = {
|
|
53
|
+
title: 'Identity/ProfileGate',
|
|
54
|
+
component: ProfileGate,
|
|
55
|
+
};
|
|
56
|
+
export default meta;
|
|
57
|
+
|
|
58
|
+
type Story = StoryObj<typeof ProfileGate>;
|
|
59
|
+
|
|
60
|
+
/** An unfinished profile: every page becomes its edit form. */
|
|
61
|
+
export const Held: Story = {
|
|
62
|
+
decorators: [
|
|
63
|
+
at('/news'),
|
|
64
|
+
withStore({
|
|
65
|
+
userSession: { token: 'a-token' },
|
|
66
|
+
myProfile: profileState('incomplete', PROFILE),
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Already on the profile. Redirecting here would be a loop with no exit. */
|
|
72
|
+
export const OnTheProfileAlready: Story = {
|
|
73
|
+
decorators: [
|
|
74
|
+
at('/identity-profiles/alice/edit'),
|
|
75
|
+
withStore({
|
|
76
|
+
userSession: { token: 'a-token' },
|
|
77
|
+
myProfile: profileState('incomplete', PROFILE),
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Signing out has to stay possible for somebody who would rather leave. */
|
|
83
|
+
export const SigningOut: Story = {
|
|
84
|
+
decorators: [
|
|
85
|
+
at('/logout'),
|
|
86
|
+
withStore({
|
|
87
|
+
userSession: { token: 'a-token' },
|
|
88
|
+
myProfile: profileState('incomplete', PROFILE),
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A finished profile goes where it was going. */
|
|
94
|
+
export const LetThrough: Story = {
|
|
95
|
+
decorators: [
|
|
96
|
+
at('/news'),
|
|
97
|
+
withStore({
|
|
98
|
+
userSession: { token: 'a-token' },
|
|
99
|
+
myProfile: profileState('complete', PROFILE),
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** A user with no Profile: there is nothing to hold them for. */
|
|
105
|
+
export const NoProfile: Story = {
|
|
106
|
+
decorators: [
|
|
107
|
+
at('/news'),
|
|
108
|
+
withStore({
|
|
109
|
+
userSession: { token: 'a-token' },
|
|
110
|
+
myProfile: profileState(null, null),
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Anonymous. Nothing is asked and nothing is held. */
|
|
116
|
+
export const Anonymous: Story = {
|
|
117
|
+
decorators: [at('/news'), withStore({ userSession: {}, myProfile: LOADING })],
|
|
118
|
+
};
|