@djangocfg/layouts 2.1.422 → 2.1.424
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 +17 -17
- package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +6 -4
- package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +12 -1
- package/src/components/errors/ErrorsTracker/utils/formatters.ts +19 -5
- package/src/layouts/AuthLayout/AuthLayout.tsx +8 -11
- package/src/layouts/AuthLayout/README.md +50 -18
- package/src/layouts/AuthLayout/components/shared/AuthConsent.tsx +46 -0
- package/src/layouts/AuthLayout/components/shared/index.ts +2 -2
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -25
- package/src/layouts/AuthLayout/context.tsx +0 -4
- package/src/layouts/AuthLayout/shells/AuthShell.tsx +10 -1
- package/src/layouts/AuthLayout/shells/FullSplitShell.tsx +73 -0
- package/src/layouts/AuthLayout/shells/types.ts +14 -4
- package/src/layouts/AuthLayout/styles/auth.css +62 -40
- package/src/layouts/AuthLayout/styles/fullsplit-shell.css +163 -0
- package/src/layouts/AuthLayout/styles/split-shell.css +74 -71
- package/src/layouts/AuthLayout/types.ts +6 -6
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +26 -2
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +39 -7
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +1 -2
- package/src/layouts/ProfileLayout/README.md +34 -2
- package/src/layouts/ProfileLayout/components/EditableField.tsx +4 -0
- package/src/layouts/ProfileLayout/hooks/index.ts +1 -1
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +14 -6
- package/src/layouts/ProfileLayout/index.ts +2 -1
- package/src/layouts/ProfileLayout/types.ts +3 -2
- package/src/testing/MockAuthFormProvider.tsx +0 -3
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +0 -72
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProfileTabValueOrCustom } from '../hooks/useProfileTabs';
|
|
4
|
+
import type { ProfileSlots, ProfileTab } from '../types';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Content the dialog renders into <ProfileForm>. All optional — anything
|
|
8
|
+
* omitted in `open()` falls back to ProfileForm's own default. Lets any caller
|
|
9
|
+
* open the profile dialog with custom tabs/slots/flags in one line.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProfileDialogContent {
|
|
12
|
+
initialTab?: ProfileTabValueOrCustom;
|
|
13
|
+
tabs?: ProfileTab[];
|
|
14
|
+
slots?: ProfileSlots;
|
|
15
|
+
enable2FA?: boolean;
|
|
16
|
+
enableAPIKeys?: boolean;
|
|
17
|
+
enableDeleteAccount?: boolean;
|
|
18
|
+
title?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ProfileDialogState extends ProfileDialogContent {
|
|
6
22
|
isOpen: boolean;
|
|
7
|
-
|
|
8
|
-
open: (options?: { initialTab?: ProfileTabValue }) => void;
|
|
23
|
+
open: (options?: ProfileDialogContent) => void;
|
|
9
24
|
close: () => void;
|
|
10
25
|
toggle: () => void;
|
|
11
26
|
}
|
|
12
27
|
|
|
28
|
+
/** Content fields reset to undefined on close (keeps `isOpen` separate). */
|
|
29
|
+
const EMPTY_CONTENT: Required<{ [K in keyof ProfileDialogContent]: undefined }> = {
|
|
30
|
+
initialTab: undefined,
|
|
31
|
+
tabs: undefined,
|
|
32
|
+
slots: undefined,
|
|
33
|
+
enable2FA: undefined,
|
|
34
|
+
enableAPIKeys: undefined,
|
|
35
|
+
enableDeleteAccount: undefined,
|
|
36
|
+
title: undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
13
39
|
export const useProfileDialogStore = create<ProfileDialogState>((set) => ({
|
|
14
40
|
isOpen: false,
|
|
15
|
-
|
|
16
|
-
open: (options) =>
|
|
17
|
-
|
|
41
|
+
...EMPTY_CONTENT,
|
|
42
|
+
open: (options) =>
|
|
43
|
+
set({
|
|
44
|
+
isOpen: true,
|
|
45
|
+
// Replace (not merge) content so a previous open()'s tabs/flags don't leak.
|
|
46
|
+
...EMPTY_CONTENT,
|
|
47
|
+
...options,
|
|
48
|
+
}),
|
|
49
|
+
close: () => set({ isOpen: false, ...EMPTY_CONTENT }),
|
|
18
50
|
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
19
51
|
}));
|
|
@@ -16,7 +16,6 @@ import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from '../c
|
|
|
16
16
|
import { ProfileProvider, useProfileContext } from './context';
|
|
17
17
|
import { useProfileTabs } from '../hooks/useProfileTabs';
|
|
18
18
|
import type { ProfileFormProps } from '../types';
|
|
19
|
-
import type { ProfileTabValue } from '../hooks/useProfileTabs';
|
|
20
19
|
|
|
21
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
21
|
// Built-in tab panels
|
|
@@ -64,7 +63,7 @@ function ProfileContent({
|
|
|
64
63
|
});
|
|
65
64
|
|
|
66
65
|
const handleTabChange = React.useCallback(
|
|
67
|
-
(value: string) => setTab(value
|
|
66
|
+
(value: string) => setTab(value),
|
|
68
67
|
[setTab],
|
|
69
68
|
);
|
|
70
69
|
|
|
@@ -28,6 +28,7 @@ import { ProfileDialog, useProfileDialogStore } from '@djangocfg/layouts';
|
|
|
28
28
|
|
|
29
29
|
// Open on a specific tab
|
|
30
30
|
useProfileDialogStore.getState().open({ initialTab: 'security' });
|
|
31
|
+
// …or with custom tabs/slots — see "Opening with custom content" below.
|
|
31
32
|
|
|
32
33
|
// In your layout (lazy-loaded)
|
|
33
34
|
<ProfileDialog />
|
|
@@ -44,7 +45,7 @@ useProfileDialogStore.getState().open({ initialTab: 'security' });
|
|
|
44
45
|
| `tabs` | `[]` | Extra `ProfileTab[]` appended after built-in tabs |
|
|
45
46
|
| `slots` | — | Named slots: `headerMenuItems`, `headerBadge`, `headerAfter`, `footer` |
|
|
46
47
|
| `title` | i18n | Page title |
|
|
47
|
-
| `defaultTab` | — | Initial active tab. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
|
|
48
|
+
| `defaultTab` | — | Initial active tab. Accepts a built-in value or a custom tab's `value`. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
|
|
48
49
|
|
|
49
50
|
## Global Profile Dialog
|
|
50
51
|
|
|
@@ -62,6 +63,37 @@ open();
|
|
|
62
63
|
open({ initialTab: 'security' });
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
### Opening with custom content
|
|
67
|
+
|
|
68
|
+
`open()` accepts the same content props as `ProfileForm` (all optional), so any
|
|
69
|
+
caller can open the dialog with custom tabs / slots / flags — and start on a
|
|
70
|
+
custom tab — in one line:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
useProfileDialogStore.getState().open({
|
|
74
|
+
initialTab: 'api-keys',
|
|
75
|
+
tabs: [{ value: 'api-keys', label: 'API keys', content: <MyApiKeysTab /> }],
|
|
76
|
+
enableAPIKeys: false, // hide the built-in API Keys tab
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `open()` payload is typed as `ProfileDialogContent`:
|
|
81
|
+
|
|
82
|
+
| Field | Type | Notes |
|
|
83
|
+
|-------|------|-------|
|
|
84
|
+
| `initialTab` | `string` | Built-in (`'profile' \| 'security' \| 'api-keys'`) **or** a custom tab's `value`. |
|
|
85
|
+
| `tabs` | `ProfileTab[]` | Extra tabs appended after the built-in ones. |
|
|
86
|
+
| `slots` | `ProfileSlots` | `headerMenuItems`, `headerBadge`, `headerAfter`, `footer`. |
|
|
87
|
+
| `enable2FA` | `boolean` | Override the Security tab. |
|
|
88
|
+
| `enableAPIKeys` | `boolean` | Override the built-in API Keys tab. |
|
|
89
|
+
| `enableDeleteAccount` | `boolean` | Override Delete Account in the header dropdown. |
|
|
90
|
+
| `title` | `string` | Dialog title (the `<ProfileDialog title>` prop wins if both set). |
|
|
91
|
+
|
|
92
|
+
Each `open()` **replaces** the previous content (it does not merge), and `close()`
|
|
93
|
+
clears it — so a bare `open()` always shows the default profile. Omitted fields
|
|
94
|
+
fall through to `ProfileForm`'s own defaults, keeping `open()` / `open({ initialTab })`
|
|
95
|
+
backward compatible.
|
|
96
|
+
|
|
65
97
|
Wire it into `PrivateLayout` for global access:
|
|
66
98
|
|
|
67
99
|
```tsx
|
|
@@ -94,7 +126,7 @@ ProfileLayout/
|
|
|
94
126
|
│ └── context.tsx Root context (labels, onLogout, onFieldSave)
|
|
95
127
|
├── ProfileDialog/
|
|
96
128
|
│ ├── ProfileDialog.tsx Dialog wrapper around ProfileForm
|
|
97
|
-
│ └── store.ts Zustand store (isOpen
|
|
129
|
+
│ └── store.ts Zustand store (isOpen + ProfileDialogContent: tabs/slots/flags/initialTab/title)
|
|
98
130
|
├── hooks/
|
|
99
131
|
│ └── useProfileTabs.ts Local tab state (useState)
|
|
100
132
|
├── types.ts ProfileFormProps, ProfileTab, ProfileSlots
|
|
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
4
4
|
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
5
5
|
|
|
6
6
|
import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { toast } from '@djangocfg/ui-core/hooks';
|
|
7
8
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
9
|
|
|
9
10
|
import { useProfileContext } from '../ProfileForm/context';
|
|
@@ -52,6 +53,9 @@ export const EditableField = ({
|
|
|
52
53
|
try {
|
|
53
54
|
await onSave(editValue);
|
|
54
55
|
setIsEditing(false);
|
|
56
|
+
} catch {
|
|
57
|
+
// Keep editing mode open with the entered value so the user can retry.
|
|
58
|
+
toast.error(labels.failedToUpdate);
|
|
55
59
|
} finally {
|
|
56
60
|
setIsSaving(false);
|
|
57
61
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { useProfileTabs } from './useProfileTabs';
|
|
2
|
-
export type { ProfileTabValue, UseProfileTabsOptions } from './useProfileTabs';
|
|
2
|
+
export type { ProfileTabValue, ProfileTabValueOrCustom, UseProfileTabsOptions } from './useProfileTabs';
|
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState, useCallback } from 'react';
|
|
4
4
|
|
|
5
|
+
/** Built-in tab values. */
|
|
5
6
|
export type ProfileTabValue = 'profile' | 'security' | 'api-keys';
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Any tab value: a built-in one, or a custom tab's `value` string.
|
|
10
|
+
* `(string & {})` keeps the built-in literals as autocomplete hints while
|
|
11
|
+
* still accepting arbitrary strings (custom tabs added via `tabs`).
|
|
12
|
+
*/
|
|
13
|
+
export type ProfileTabValueOrCustom = ProfileTabValue | (string & {});
|
|
14
|
+
|
|
7
15
|
export interface UseProfileTabsOptions {
|
|
8
16
|
enable2FA?: boolean;
|
|
9
17
|
enableAPIKeys?: boolean;
|
|
10
18
|
extraTabValues?: string[];
|
|
11
|
-
/** Initial active tab. Defaults to `'profile'`. */
|
|
12
|
-
defaultTab?:
|
|
19
|
+
/** Initial active tab. Defaults to `'profile'`. Accepts custom tab values. */
|
|
20
|
+
defaultTab?: ProfileTabValueOrCustom;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
/**
|
|
@@ -25,18 +33,18 @@ export function useProfileTabs(options: UseProfileTabsOptions = {}) {
|
|
|
25
33
|
const { enable2FA, enableAPIKeys, extraTabValues = [], defaultTab } = options;
|
|
26
34
|
|
|
27
35
|
const allowed = useMemo(() => {
|
|
28
|
-
const base:
|
|
36
|
+
const base: ProfileTabValueOrCustom[] = ['profile'];
|
|
29
37
|
if (enable2FA) base.push('security');
|
|
30
38
|
if (enableAPIKeys) base.push('api-keys');
|
|
31
|
-
return [...base, ...extraTabValues]
|
|
39
|
+
return [...base, ...extraTabValues];
|
|
32
40
|
}, [enable2FA, enableAPIKeys, extraTabValues]);
|
|
33
41
|
|
|
34
|
-
const [tab, setTabState] = useState<
|
|
42
|
+
const [tab, setTabState] = useState<ProfileTabValueOrCustom>(
|
|
35
43
|
defaultTab && allowed.includes(defaultTab) ? defaultTab : 'profile',
|
|
36
44
|
);
|
|
37
45
|
|
|
38
46
|
const setTab = useCallback(
|
|
39
|
-
(value:
|
|
47
|
+
(value: ProfileTabValueOrCustom) => {
|
|
40
48
|
if (allowed.includes(value)) {
|
|
41
49
|
setTabState(value);
|
|
42
50
|
}
|
|
@@ -2,6 +2,7 @@ export { ProfileForm, useProfileContext } from './ProfileForm';
|
|
|
2
2
|
export { ProfileProvider } from './ProfileForm/context';
|
|
3
3
|
export { useProfileTabs } from './hooks';
|
|
4
4
|
export { ProfileDialog, useProfileDialogStore } from './ProfileDialog';
|
|
5
|
+
export type { ProfileDialogProps, ProfileDialogContent } from './ProfileDialog';
|
|
5
6
|
export type { ProfileLabels } from './ProfileForm/context';
|
|
6
7
|
export type { ProfileFormProps, ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
|
|
7
|
-
export type { ProfileTabValue, UseProfileTabsOptions } from './hooks';
|
|
8
|
+
export type { ProfileTabValue, ProfileTabValueOrCustom, UseProfileTabsOptions } from './hooks';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProfileTabValueOrCustom } from './hooks/useProfileTabs';
|
|
4
4
|
|
|
5
5
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
6
|
// Slot + Tab types
|
|
@@ -39,8 +39,9 @@ export interface ProfileFormProps {
|
|
|
39
39
|
/**
|
|
40
40
|
* When provided, the active tab is controlled locally (no URL sync).
|
|
41
41
|
* Useful for dialogs where query-string pollution is undesirable.
|
|
42
|
+
* Accepts a built-in value or a custom tab's `value`.
|
|
42
43
|
*/
|
|
43
|
-
defaultTab?:
|
|
44
|
+
defaultTab?: ProfileTabValueOrCustom;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/** @deprecated Use ProfileFormProps instead */
|
|
@@ -53,7 +53,6 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
53
53
|
termsUrl,
|
|
54
54
|
privacyUrl,
|
|
55
55
|
enableGithubAuth = false,
|
|
56
|
-
enable2FASetup = true,
|
|
57
56
|
logoUrl,
|
|
58
57
|
redirectUrl = '/dashboard',
|
|
59
58
|
|
|
@@ -152,7 +151,6 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
152
151
|
termsUrl,
|
|
153
152
|
privacyUrl,
|
|
154
153
|
enableGithubAuth,
|
|
155
|
-
enable2FASetup,
|
|
156
154
|
logoUrl,
|
|
157
155
|
redirectUrl,
|
|
158
156
|
};
|
|
@@ -176,7 +174,6 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
176
174
|
termsUrl,
|
|
177
175
|
privacyUrl,
|
|
178
176
|
enableGithubAuth,
|
|
179
|
-
enable2FASetup,
|
|
180
177
|
logoUrl,
|
|
181
178
|
redirectUrl,
|
|
182
179
|
]);
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { memo, useMemo } from 'react';
|
|
4
|
-
|
|
5
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
6
|
-
import { Checkbox } from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
export interface TermsCheckboxProps {
|
|
9
|
-
checked: boolean;
|
|
10
|
-
onChange: (checked: boolean) => void;
|
|
11
|
-
termsUrl?: string;
|
|
12
|
-
privacyUrl?: string;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
className?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* TermsCheckbox - Compact terms acceptance checkbox.
|
|
19
|
-
*
|
|
20
|
-
* Memoised: re-renders only when checked, termsUrl, privacyUrl, disabled
|
|
21
|
-
* or className change. `onChange` is compared by reference — callers
|
|
22
|
-
* should stabilise it with useCallback.
|
|
23
|
-
*/
|
|
24
|
-
function TermsCheckboxRaw({
|
|
25
|
-
checked,
|
|
26
|
-
onChange,
|
|
27
|
-
termsUrl,
|
|
28
|
-
privacyUrl,
|
|
29
|
-
disabled = false,
|
|
30
|
-
className = '',
|
|
31
|
-
}: TermsCheckboxProps) {
|
|
32
|
-
const t = useAppT();
|
|
33
|
-
const labels = useMemo(() => ({
|
|
34
|
-
agree: t('layouts.auth.terms.agree'),
|
|
35
|
-
and: t('layouts.auth.terms.and'),
|
|
36
|
-
terms: t('layouts.auth.terms.terms'),
|
|
37
|
-
privacy: t('layouts.auth.terms.privacy'),
|
|
38
|
-
}), [t]);
|
|
39
|
-
|
|
40
|
-
// Don't render if no links provided
|
|
41
|
-
if (!termsUrl && !privacyUrl) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div className={`auth-terms ${className}`}>
|
|
47
|
-
<Checkbox
|
|
48
|
-
id="auth-terms"
|
|
49
|
-
checked={checked}
|
|
50
|
-
onCheckedChange={onChange}
|
|
51
|
-
disabled={disabled}
|
|
52
|
-
className="auth-terms-checkbox"
|
|
53
|
-
/>
|
|
54
|
-
<label htmlFor="auth-terms">
|
|
55
|
-
{labels.agree}{' '}
|
|
56
|
-
{termsUrl && (
|
|
57
|
-
<a href={termsUrl} target="_blank" rel="noopener noreferrer">
|
|
58
|
-
{labels.terms}
|
|
59
|
-
</a>
|
|
60
|
-
)}
|
|
61
|
-
{termsUrl && privacyUrl && ` ${labels.and} `}
|
|
62
|
-
{privacyUrl && (
|
|
63
|
-
<a href={privacyUrl} target="_blank" rel="noopener noreferrer">
|
|
64
|
-
{labels.privacy}
|
|
65
|
-
</a>
|
|
66
|
-
)}
|
|
67
|
-
</label>
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const TermsCheckbox = memo(TermsCheckboxRaw);
|