@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,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
+ };