@djangocfg/layouts 2.1.601 → 2.1.603
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/package.json +12 -12
- package/src/components/errors/ErrorsTracker/components/ErrorButtons.tsx +2 -2
- package/src/layouts/AuthLayout/README.md +22 -0
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +25 -60
- package/src/layouts/AuthLayout/components/oauth/OAuthNotice.tsx +94 -0
- package/src/layouts/AuthLayout/components/oauth/index.ts +1 -0
- package/src/layouts/AuthLayout/components/oauth/oauthErrors.ts +1 -42
- package/src/layouts/AuthLayout/components/oauth/oauthNoticeCopy.test.ts +36 -0
- package/src/layouts/AuthLayout/components/oauth/oauthNoticeCopy.ts +86 -0
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +15 -2
- package/src/layouts/AuthLayout/context.tsx +7 -2
- package/src/layouts/AuthLayout/index.ts +1 -0
- package/src/layouts/AuthLayout/types.ts +10 -1
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +2 -0
- package/src/layouts/PrivateLayout/README.md +5 -1
- package/src/layouts/PrivateLayout/components/SidebarBody.tsx +3 -1
- package/src/layouts/PrivateLayout/components/SidebarNav.test.tsx +152 -0
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +85 -45
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +54 -13
- package/src/layouts/PrivateLayout/context.tsx +5 -3
- package/src/layouts/PrivateLayout/index.ts +4 -0
- package/src/layouts/PrivateLayout/mcpSidebar.ts +35 -0
- package/src/layouts/PrivateLayout/sidebarItems.test.ts +58 -0
- package/src/layouts/PrivateLayout/sidebarItems.ts +38 -0
- package/src/layouts/PrivateLayout/types.ts +30 -7
- package/src/layouts/SettingsLayout/README.md +21 -0
- package/src/layouts/SettingsLayout/components/AvatarSection.tsx +0 -1
- package/src/layouts/SettingsLayout/hooks/index.ts +3 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +11 -8
- package/src/layouts/SettingsLayout/hooks/useSettingsSectionState.ts +32 -0
- package/src/layouts/SettingsLayout/index.ts +4 -0
- package/src/layouts/SettingsLayout/sectionIds.test-d.ts +51 -0
- package/src/layouts/SettingsLayout/sectionIds.test.ts +31 -0
- package/src/layouts/SettingsLayout/sectionIds.ts +43 -0
- package/src/testing/MockAuthFormProvider.tsx +5 -1
|
@@ -15,9 +15,8 @@ import type { UserMenuConfig } from '../types';
|
|
|
15
15
|
// Sidebar Item Types
|
|
16
16
|
// ============================================================================
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
interface SidebarItemBase {
|
|
19
19
|
label: string;
|
|
20
|
-
href: string;
|
|
21
20
|
icon?: string | LucideIcon;
|
|
22
21
|
badge?: string | number;
|
|
23
22
|
/** Visual style of `badge`: `'count'` (default, neutral pill) or `'pill'` (accent-tinted, e.g. "lite"/"new"). */
|
|
@@ -26,15 +25,39 @@ export interface SidebarItem {
|
|
|
26
25
|
tooltip?: string;
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
/** A row that navigates to a route. */
|
|
29
|
+
export interface SidebarLinkItem extends SidebarItemBase {
|
|
30
|
+
href: string;
|
|
31
|
+
settingsSection?: never;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A row that opens the Settings dialog on a section. It renders only while the mounted `SettingsDialog` lists that
|
|
36
|
+
* section, so there is no dead door when a section is switched off or not registered yet; it is active while the
|
|
37
|
+
* dialog shows the section. `Id` is the section-id union (see `defineSettingsSections`); a bare `SidebarItem` takes any string.
|
|
38
|
+
*/
|
|
39
|
+
export interface SidebarSettingsItem<Id extends string = string> extends SidebarItemBase {
|
|
40
|
+
settingsSection: Id;
|
|
41
|
+
href?: never;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A sidebar row: a link (`href`) or a Settings launcher (`settingsSection`). */
|
|
45
|
+
export type SidebarItem<Id extends string = string> = SidebarLinkItem | SidebarSettingsItem<Id>;
|
|
46
|
+
|
|
29
47
|
// ============================================================================
|
|
30
48
|
// Sidebar Group Types
|
|
31
49
|
// ============================================================================
|
|
32
50
|
|
|
33
|
-
export interface SidebarGroupConfig {
|
|
51
|
+
export interface SidebarGroupConfig<Id extends string = string> {
|
|
34
52
|
/** Group label displayed above items */
|
|
35
53
|
label: string;
|
|
36
|
-
/** Items in this group */
|
|
37
|
-
items: SidebarItem[];
|
|
54
|
+
/** Items in this group. A group whose items are all `settingsSection` rows for unregistered sections is not rendered. */
|
|
55
|
+
items: SidebarItem<Id>[];
|
|
56
|
+
/**
|
|
57
|
+
* A secondary group (setup, connect): a full divider with extra space above it, and the label below the divider if
|
|
58
|
+
* there is one. The default groups are only a hairline apart. Works in the docked sidebar, the peek panel and the drawer.
|
|
59
|
+
*/
|
|
60
|
+
separated?: boolean;
|
|
38
61
|
/** If true, group is only shown when it has items (for dynamic groups like extensions) */
|
|
39
62
|
dynamic?: boolean;
|
|
40
63
|
/** Render group as an accordion (Mailersend/Vercel-style). Label becomes a clickable trigger. */
|
|
@@ -73,9 +96,9 @@ export interface SidebarFeaturedConfig {
|
|
|
73
96
|
// Sidebar Config
|
|
74
97
|
// ============================================================================
|
|
75
98
|
|
|
76
|
-
export interface SidebarConfig {
|
|
99
|
+
export interface SidebarConfig<Id extends string = string> {
|
|
77
100
|
/** Grouped items with labels */
|
|
78
|
-
groups: SidebarGroupConfig[];
|
|
101
|
+
groups: SidebarGroupConfig<Id>[];
|
|
79
102
|
/** Home link href */
|
|
80
103
|
homeHref?: string;
|
|
81
104
|
/**
|
|
@@ -64,6 +64,27 @@ const settings = useSettingsDialog();
|
|
|
64
64
|
// #settings/api-keys → opens on the API-keys section
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### Typed section ids
|
|
68
|
+
|
|
69
|
+
`useSettingsDialog().open(id)` takes any string by default, so a typo opens the first section silently. Declare the
|
|
70
|
+
sections with `defineSettingsSections` and give the hook the id union; an unknown id is then a compile error.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
const sections = defineSettingsSections([mcpSettingsSection(), { id: 'billing', label: 'Billing', content: <Billing /> }]);
|
|
74
|
+
type AppSection = SettingsSectionId<typeof sections>; // 'mcp' | 'billing' | 'account' | 'general' | 'api-keys'
|
|
75
|
+
|
|
76
|
+
<PrivateLayout settings={{ sections }} sidebar={{ groups: [
|
|
77
|
+
{ label: 'Workspace', items: [{ label: 'Billing', settingsSection: 'billing' } satisfies SidebarItem<AppSection>] },
|
|
78
|
+
mcpSidebarGroup({ label: 'Connect' }), // separated group, appears only if 'mcp' is registered
|
|
79
|
+
] }}>
|
|
80
|
+
const { open } = useSettingsDialog<AppSection>(); // open('biling') does not compile
|
|
81
|
+
<a href={settingsHref<AppSection>('billing')}>Billing</a> // "#settings/billing"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`mcpSettingsSection()` returns `id: 'mcp'` as a literal, so it keeps its place in the union; a section whose `id` is a
|
|
85
|
+
plain `string` widens the union to `string` (the old behaviour). Existing `useSettingsDialog()` callers are unchanged.
|
|
86
|
+
`settingsHref(id, urlKey = 'settings')` is the one place that spells the `#settings/<id>` fragment.
|
|
87
|
+
|
|
67
88
|
### Inline page
|
|
68
89
|
|
|
69
90
|
```tsx
|
|
@@ -13,3 +13,6 @@ export type {
|
|
|
13
13
|
|
|
14
14
|
export { useAccountMenuItems } from './useAccountMenuItems';
|
|
15
15
|
export type { UseAccountMenuItemsOptions } from './useAccountMenuItems';
|
|
16
|
+
|
|
17
|
+
export { useSettingsSectionState } from './useSettingsSectionState';
|
|
18
|
+
export type { SettingsSectionState } from './useSettingsSectionState';
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* <Button onClick={() => settings.open()}>Settings</Button>
|
|
10
10
|
* <MenuItem onClick={() => settings.open('billing')}>Billing</MenuItem>
|
|
11
11
|
*
|
|
12
|
+
* The id is `string` by default. Pass the union of your sections to make a typo a compile error:
|
|
13
|
+
* const settings = useSettingsDialog<SettingsSectionId<typeof sections>>(); // see `defineSettingsSections`
|
|
14
|
+
*
|
|
12
15
|
* Returns stable callbacks, safe in deps arrays. The dialog itself must be
|
|
13
16
|
* mounted once in the tree (PrivateLayout does this).
|
|
14
17
|
*/
|
|
@@ -17,24 +20,24 @@ import { useCallback, useMemo } from 'react';
|
|
|
17
20
|
|
|
18
21
|
import { useSettingsDialogStore, type SettingsMenuSection } from '../store';
|
|
19
22
|
|
|
20
|
-
export interface UseSettingsDialogReturn {
|
|
23
|
+
export interface UseSettingsDialogReturn<Id extends string = string> {
|
|
21
24
|
/** Whether the dialog is currently open. */
|
|
22
25
|
isOpen: boolean;
|
|
23
26
|
/** Currently active section id (null = fall back to first). */
|
|
24
|
-
activeSection:
|
|
27
|
+
activeSection: Id | null;
|
|
25
28
|
/** Open the dialog, optionally on a specific section. */
|
|
26
|
-
open: (sectionId?:
|
|
29
|
+
open: (sectionId?: Id) => void;
|
|
27
30
|
/** Close the dialog. */
|
|
28
31
|
close: () => void;
|
|
29
32
|
/** Toggle open/closed. */
|
|
30
33
|
toggle: () => void;
|
|
31
34
|
/** Switch the active section. */
|
|
32
|
-
setSection: (sectionId:
|
|
35
|
+
setSection: (sectionId: Id) => void;
|
|
33
36
|
/** The visible sections of the mounted dialog (built-in and the host's), in rail order: for menu shortcuts, `open(id)`. */
|
|
34
37
|
sections: SettingsMenuSection[];
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
export function useSettingsDialog(): UseSettingsDialogReturn {
|
|
40
|
+
export function useSettingsDialog<Id extends string = string>(): UseSettingsDialogReturn<Id> {
|
|
38
41
|
const isOpen = useSettingsDialogStore((s) => s.isOpen);
|
|
39
42
|
const activeSection = useSettingsDialogStore((s) => s.activeSection);
|
|
40
43
|
const storeOpen = useSettingsDialogStore((s) => s.open);
|
|
@@ -43,13 +46,13 @@ export function useSettingsDialog(): UseSettingsDialogReturn {
|
|
|
43
46
|
const storeSetSection = useSettingsDialogStore((s) => s.setSection);
|
|
44
47
|
const sections = useSettingsDialogStore((s) => s.sections);
|
|
45
48
|
|
|
46
|
-
const open = useCallback((sectionId?:
|
|
49
|
+
const open = useCallback((sectionId?: Id) => storeOpen(sectionId), [storeOpen]);
|
|
47
50
|
const close = useCallback(() => storeClose(), [storeClose]);
|
|
48
51
|
const toggle = useCallback(() => storeToggle(), [storeToggle]);
|
|
49
|
-
const setSection = useCallback((id:
|
|
52
|
+
const setSection = useCallback((id: Id) => storeSetSection(id), [storeSetSection]);
|
|
50
53
|
|
|
51
54
|
return useMemo(
|
|
52
|
-
() => ({ isOpen, activeSection, open, close, toggle, setSection, sections }),
|
|
55
|
+
() => ({ isOpen, activeSection: activeSection as Id | null, open, close, toggle, setSection, sections }),
|
|
53
56
|
[isOpen, activeSection, open, close, toggle, setSection, sections],
|
|
54
57
|
);
|
|
55
58
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useSettingsSectionState — what a launcher of one Settings section needs to know: is the section registered with the
|
|
5
|
+
* mounted dialog (a launcher for a missing section is a dead door), is the dialog showing it now, and how to open it.
|
|
6
|
+
*
|
|
7
|
+
* Reads the same store `useSettingsDialog` does. Registration comes from `<SettingsDialog />`, which lists its visible
|
|
8
|
+
* sections (built-in and the host's) after it mounts, so before that, or with no dialog, nothing is registered.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback } from 'react';
|
|
12
|
+
|
|
13
|
+
import { useSettingsDialogStore } from '../store';
|
|
14
|
+
|
|
15
|
+
export interface SettingsSectionState {
|
|
16
|
+
/** The dialog lists this section in its rail. */
|
|
17
|
+
registered: boolean;
|
|
18
|
+
/** The dialog is open and showing this section (an open dialog with no pick shows the first one). */
|
|
19
|
+
active: boolean;
|
|
20
|
+
/** Open the dialog on this section. */
|
|
21
|
+
open: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useSettingsSectionState(id: string): SettingsSectionState {
|
|
25
|
+
const registered = useSettingsDialogStore((s) => s.sections.some((section) => section.id === id));
|
|
26
|
+
const active = useSettingsDialogStore(
|
|
27
|
+
(s) => s.isOpen && (s.activeSection ?? s.sections[0]?.id ?? null) === id,
|
|
28
|
+
);
|
|
29
|
+
const storeOpen = useSettingsDialogStore((s) => s.open);
|
|
30
|
+
const open = useCallback(() => storeOpen(id), [storeOpen, id]);
|
|
31
|
+
return { registered, active, open };
|
|
32
|
+
}
|
|
@@ -43,6 +43,10 @@ export type {
|
|
|
43
43
|
ResolvedGroup,
|
|
44
44
|
} from './hooks';
|
|
45
45
|
|
|
46
|
+
// Typed section ids, the `#settings/<id>` address
|
|
47
|
+
export { defineSettingsSections, settingsHref, MCP_SECTION_ID } from './sectionIds';
|
|
48
|
+
export type { SettingsSectionId, BuiltinSettingsSectionId } from './sectionIds';
|
|
49
|
+
|
|
46
50
|
// Context (for advanced custom shells)
|
|
47
51
|
export { SettingsProvider, useSettingsContext } from './context';
|
|
48
52
|
export type { SettingsProviderProps } from './context';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type tests: tsc (vitest typecheck, and `pnpm check`) must accept the first block and reject every
|
|
3
|
+
* `@ts-expect-error` line. A line that stops being an error fails the build.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentType } from 'react';
|
|
7
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { MCP_SECTION_ID, defineSettingsSections, settingsHref, type SettingsSectionId } from './sectionIds';
|
|
10
|
+
import { useSettingsDialog } from './hooks/useSettingsDialog';
|
|
11
|
+
import type { SettingsSection } from './types';
|
|
12
|
+
|
|
13
|
+
declare const mcpLike: { id: 'mcp'; label: string; content: null; icon: ComponentType<{ className?: string }> };
|
|
14
|
+
|
|
15
|
+
const sections = defineSettingsSections([mcpLike, { id: 'billing', label: 'Billing', content: null }]);
|
|
16
|
+
type AppId = SettingsSectionId<typeof sections>;
|
|
17
|
+
|
|
18
|
+
describe('typed settings section ids', () => {
|
|
19
|
+
it('the id union is the declared ids plus the built-ins', () => {
|
|
20
|
+
expectTypeOf<AppId>().toEqualTypeOf<'mcp' | 'billing' | 'account' | 'general' | 'api-keys'>();
|
|
21
|
+
expectTypeOf(MCP_SECTION_ID).toEqualTypeOf<'mcp'>();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('the result still goes where a SettingsSection[] goes', () => {
|
|
25
|
+
expectTypeOf(sections).toMatchTypeOf<SettingsSection[]>();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('useSettingsDialog<Id> accepts known ids only', () => {
|
|
29
|
+
const typed = useSettingsDialog<AppId>();
|
|
30
|
+
typed.open('billing');
|
|
31
|
+
typed.setSection('mcp');
|
|
32
|
+
// @ts-expect-error unknown id
|
|
33
|
+
typed.open('biling');
|
|
34
|
+
// @ts-expect-error unknown id
|
|
35
|
+
typed.setSection('nope');
|
|
36
|
+
expectTypeOf(typed.activeSection).toEqualTypeOf<AppId | null>();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('the untyped default still takes any string', () => {
|
|
40
|
+
const loose = useSettingsDialog();
|
|
41
|
+
loose.open('anything');
|
|
42
|
+
loose.setSection('else');
|
|
43
|
+
expectTypeOf(loose.activeSection).toEqualTypeOf<string | null>();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('settingsHref keeps the id in the type', () => {
|
|
47
|
+
expectTypeOf(settingsHref('mcp')).toEqualTypeOf<`#${string}/mcp`>();
|
|
48
|
+
// @ts-expect-error the helper for a typed id rejects other strings
|
|
49
|
+
settingsHref<AppId>('biling');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { parseSettingsHash } from './intent/parseSettingsHash';
|
|
4
|
+
import { MCP_SECTION_ID, defineSettingsSections, settingsHref } from './sectionIds';
|
|
5
|
+
|
|
6
|
+
describe('settingsHref', () => {
|
|
7
|
+
it('spells the fragment the dialog reads', () => {
|
|
8
|
+
expect(settingsHref('mcp')).toBe('#settings/mcp');
|
|
9
|
+
expect(settingsHref('api-keys')).toBe('#settings/api-keys');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('follows a custom urlKey', () => {
|
|
13
|
+
expect(settingsHref('mcp', 'prefs')).toBe('#prefs/mcp');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('round-trips through the parser', () => {
|
|
17
|
+
const fragment = settingsHref('billing').slice(1);
|
|
18
|
+
expect(parseSettingsHash(fragment, 'settings')).toMatchObject({ open: true, section: 'billing' });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('defineSettingsSections', () => {
|
|
23
|
+
it('returns the very array it was given', () => {
|
|
24
|
+
const list = [{ id: 'billing', label: 'Billing', content: null }];
|
|
25
|
+
expect(defineSettingsSections(list)).toBe(list);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('MCP_SECTION_ID is the id the MCP widget uses', () => {
|
|
29
|
+
expect(MCP_SECTION_ID).toBe('mcp');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed section ids.
|
|
3
|
+
*
|
|
4
|
+
* The dialog is mounted once and driven through a global store, so its `sections` prop cannot type the hooks that
|
|
5
|
+
* open it. Declare the sections with `defineSettingsSections` and hand the id union to the hook instead:
|
|
6
|
+
*
|
|
7
|
+
* const sections = defineSettingsSections([mcpSettingsSection(), { id: 'billing', ... }]);
|
|
8
|
+
* type AppSection = SettingsSectionId<typeof sections>; // 'mcp' | 'billing' | built-in ids
|
|
9
|
+
* useSettingsDialog<AppSection>().open('biling'); // compile error
|
|
10
|
+
*
|
|
11
|
+
* Nothing here is runtime weight beyond two one-line functions.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { SettingsSection } from './types';
|
|
15
|
+
|
|
16
|
+
/** Ids of the sections the layout ships (`builtins`). They are openable whether or not the host switched one off. */
|
|
17
|
+
export type BuiltinSettingsSectionId = 'account' | 'general' | 'api-keys';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The id of the MCP section. `@djangocfg/widget-mcp` (`mcpSettingsSection()`) uses the same literal; layouts cannot
|
|
21
|
+
* import the widget (a package never depends on a widget), so the two agree by value and each side has a test on it.
|
|
22
|
+
*/
|
|
23
|
+
export const MCP_SECTION_ID = 'mcp' as const;
|
|
24
|
+
|
|
25
|
+
/** The ids of a section list: the host's own plus the built-ins. Collapses to `string` if any id is not a literal. */
|
|
26
|
+
export type SettingsSectionId<S extends readonly { id: string }[]> = S[number]['id'] | BuiltinSettingsSectionId;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Identity function that keeps each section's `id` as a literal type, so `SettingsSectionId<typeof sections>` is a
|
|
30
|
+
* closed union. Accepts the result of `mcpSettingsSection()` as is.
|
|
31
|
+
*/
|
|
32
|
+
export function defineSettingsSections<const S extends SettingsSection[]>(sections: S): S {
|
|
33
|
+
return sections;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The address that opens the dialog on a section: `settingsHref('mcp')` is `"#settings/mcp"`. For a plain link
|
|
38
|
+
* (`href`, `<a>`), a route table or an email; `useSettingsUrl` opens the dialog when the page lands on it.
|
|
39
|
+
* `urlKey` follows `SettingsDialog`'s `urlKey` prop.
|
|
40
|
+
*/
|
|
41
|
+
export function settingsHref<Id extends string = string>(id: Id, urlKey = 'settings'): `#${string}/${Id}` {
|
|
42
|
+
return `#${urlKey}/${id}`;
|
|
43
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { AuthFormContext } from '../layouts/AuthLayout/context';
|
|
14
14
|
import { resolveOAuthProviders } from '../layouts/AuthLayout/components/oauth/oauthProviders';
|
|
15
|
-
import type { AuthFormContextType, AuthLayoutConfig } from '../layouts/AuthLayout/types';
|
|
15
|
+
import type { AuthFormContextType, AuthLayoutConfig, OAuthFailure } from '../layouts/AuthLayout/types';
|
|
16
16
|
|
|
17
17
|
// Storybook-only provider. Reproduces the AuthFormContext shape with
|
|
18
18
|
// in-memory state and no-op async handlers, so step components and the
|
|
@@ -117,6 +117,7 @@ const MockAuthFormProviderBase: React.FC<MockAuthFormProviderProps> = ({
|
|
|
117
117
|
);
|
|
118
118
|
|
|
119
119
|
const isAutoSubmittingFromUrl = useRef(false);
|
|
120
|
+
const [oauthNotice, setOAuthNotice] = useState<OAuthFailure | null>(null);
|
|
120
121
|
|
|
121
122
|
const value: AuthFormContextType = useMemo(() => {
|
|
122
123
|
const stop = (e: React.FormEvent) => {
|
|
@@ -177,6 +178,8 @@ const MockAuthFormProviderBase: React.FC<MockAuthFormProviderProps> = ({
|
|
|
177
178
|
termsUrl,
|
|
178
179
|
privacyUrl,
|
|
179
180
|
oauth,
|
|
181
|
+
oauthNotice,
|
|
182
|
+
setOAuthNotice,
|
|
180
183
|
logo,
|
|
181
184
|
redirectUrl,
|
|
182
185
|
marketingConsentConfig,
|
|
@@ -197,6 +200,7 @@ const MockAuthFormProviderBase: React.FC<MockAuthFormProviderProps> = ({
|
|
|
197
200
|
termsUrl,
|
|
198
201
|
privacyUrl,
|
|
199
202
|
oauth,
|
|
203
|
+
oauthNotice,
|
|
200
204
|
logo,
|
|
201
205
|
redirectUrl,
|
|
202
206
|
marketingConsentConfig,
|