@djangocfg/layouts 2.1.356 → 2.1.358

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 (83) hide show
  1. package/package.json +21 -19
  2. package/src/configurator/private/schema.ts +12 -0
  3. package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
  4. package/src/layouts/AppLayout/AppLayout.tsx +35 -15
  5. package/src/layouts/AppLayout/BaseApp.tsx +2 -2
  6. package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
  7. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
  8. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
  9. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
  10. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
  11. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
  12. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
  13. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
  14. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
  16. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
  17. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
  18. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
  23. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
  24. package/src/layouts/AuthLayout/context.tsx +35 -13
  25. package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
  26. package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
  27. package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
  28. package/src/layouts/AuthLayout/shells/context.tsx +16 -5
  29. package/src/layouts/PrivateLayout/PrivateLayout.tsx +45 -248
  30. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +113 -430
  31. package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +82 -105
  32. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +168 -0
  33. package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
  34. package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
  35. package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
  36. package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
  37. package/src/layouts/PrivateLayout/components/index.ts +4 -0
  38. package/src/layouts/PrivateLayout/context.tsx +211 -0
  39. package/src/layouts/PrivateLayout/density.ts +48 -0
  40. package/src/layouts/PrivateLayout/hooks/index.ts +14 -0
  41. package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
  42. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +110 -0
  43. package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
  44. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
  45. package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +21 -0
  46. package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
  47. package/src/layouts/PrivateLayout/index.ts +2 -2
  48. package/src/layouts/PrivateLayout/types.ts +193 -0
  49. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
  50. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
  51. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
  52. package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +8 -8
  53. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +148 -0
  54. package/src/layouts/ProfileLayout/README.md +118 -0
  55. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
  56. package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
  57. package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
  58. package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
  59. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
  60. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
  61. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +35 -0
  62. package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
  63. package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
  64. package/src/layouts/ProfileLayout/components/index.ts +5 -2
  65. package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
  66. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +48 -0
  67. package/src/layouts/ProfileLayout/index.ts +7 -3
  68. package/src/layouts/ProfileLayout/types.ts +47 -0
  69. package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
  70. package/src/layouts/PublicLayout/components/index.ts +4 -0
  71. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
  72. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
  73. package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
  74. package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
  75. package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
  76. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
  77. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
  78. package/src/layouts/_components/index.ts +2 -6
  79. package/src/layouts/index.ts +9 -4
  80. package/src/layouts/ProfileLayout/ProfileLayout.tsx +0 -284
  81. package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
  82. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
  83. /package/src/layouts/{_components → PublicLayout/components}/UserAvatar.tsx +0 -0
@@ -1,234 +0,0 @@
1
- /**
2
- * Tests for TwoFactorSection component
3
- *
4
- * Tests the 2FA management section in ProfileLayout.
5
- */
6
-
7
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
8
- import React from 'react';
9
-
10
- import { TwoFactorSection } from '../components/TwoFactorSection';
11
-
12
- // Mock hooks
13
- const mockFetchStatus = jest.fn();
14
- const mockDisable2FA = jest.fn();
15
- const mockResetSetup = jest.fn();
16
- const mockClearError = jest.fn();
17
-
18
- jest.mock('@djangocfg/api/auth', () => ({
19
- useTwoFactorStatus: () => ({
20
- isLoading: false,
21
- error: null,
22
- has2FAEnabled: false,
23
- devices: [],
24
- fetchStatus: mockFetchStatus,
25
- disable2FA: mockDisable2FA,
26
- clearError: mockClearError,
27
- }),
28
- useTwoFactorSetup: () => ({
29
- resetSetup: mockResetSetup,
30
- }),
31
- }));
32
-
33
- // Mock TwoFactorSetup component
34
- jest.mock('../../AuthLayout/components/TwoFactorSetup', () => ({
35
- TwoFactorSetup: ({ onComplete, onSkip }: any) => (
36
- <div data-testid="two-factor-setup">
37
- <button onClick={onComplete}>Complete Setup</button>
38
- <button onClick={onSkip}>Skip Setup</button>
39
- </div>
40
- ),
41
- }));
42
-
43
- // Mock UI components
44
- jest.mock('@djangocfg/ui-core/components', () => ({
45
- Alert: ({ children, variant }: any) => (
46
- <div data-testid="alert" data-variant={variant}>
47
- {children}
48
- </div>
49
- ),
50
- AlertDescription: ({ children }: any) => <span>{children}</span>,
51
- Button: ({ children, onClick, disabled, variant }: any) => (
52
- <button onClick={onClick} disabled={disabled} data-variant={variant}>
53
- {children}
54
- </button>
55
- ),
56
- Card: ({ children, className }: any) => (
57
- <div data-testid="card" className={className}>
58
- {children}
59
- </div>
60
- ),
61
- CardContent: ({ children }: any) => <div>{children}</div>,
62
- CardDescription: ({ children }: any) => <p>{children}</p>,
63
- CardHeader: ({ children }: any) => <div>{children}</div>,
64
- CardTitle: ({ children }: any) => <h3>{children}</h3>,
65
- Dialog: ({ children, open }: any) => (open ? <div data-testid="dialog">{children}</div> : null),
66
- DialogContent: ({ children }: any) => <div>{children}</div>,
67
- DialogDescription: ({ children }: any) => <p>{children}</p>,
68
- DialogFooter: ({ children }: any) => <div>{children}</div>,
69
- DialogHeader: ({ children }: any) => <div>{children}</div>,
70
- DialogTitle: ({ children }: any) => <h4>{children}</h4>,
71
- OTPInput: ({ value, onChange, disabled }: any) => (
72
- <input
73
- data-testid="otp-input"
74
- value={value}
75
- onChange={(e) => onChange(e.target.value)}
76
- disabled={disabled}
77
- />
78
- ),
79
- }));
80
-
81
- describe('TwoFactorSection', () => {
82
- beforeEach(() => {
83
- jest.clearAllMocks();
84
- });
85
-
86
- describe('when 2FA is disabled', () => {
87
- it('should render disabled state', () => {
88
- render(<TwoFactorSection />);
89
-
90
- expect(screen.getByText('Two-Factor Authentication')).toBeInTheDocument();
91
- expect(screen.getByText('2FA is not enabled')).toBeInTheDocument();
92
- expect(screen.getByText('Enable 2FA')).toBeInTheDocument();
93
- });
94
-
95
- it('should fetch status on mount', () => {
96
- render(<TwoFactorSection />);
97
-
98
- expect(mockFetchStatus).toHaveBeenCalledTimes(1);
99
- });
100
-
101
- it('should show security recommendation', () => {
102
- render(<TwoFactorSection />);
103
-
104
- expect(
105
- screen.getByText(/Two-factor authentication adds an extra layer of security/)
106
- ).toBeInTheDocument();
107
- });
108
- });
109
-
110
- describe('when 2FA is enabled', () => {
111
- beforeEach(() => {
112
- jest.doMock('@djangocfg/api/auth', () => ({
113
- useTwoFactorStatus: () => ({
114
- isLoading: false,
115
- error: null,
116
- has2FAEnabled: true,
117
- devices: [
118
- {
119
- id: '123',
120
- name: 'My Authenticator',
121
- createdAt: '2024-01-01T00:00:00Z',
122
- lastUsedAt: null,
123
- isPrimary: true,
124
- },
125
- ],
126
- fetchStatus: mockFetchStatus,
127
- disable2FA: mockDisable2FA,
128
- clearError: mockClearError,
129
- }),
130
- useTwoFactorSetup: () => ({
131
- resetSetup: mockResetSetup,
132
- }),
133
- }));
134
- });
135
-
136
- it('should render enabled state with device info', async () => {
137
- // Note: Due to jest module caching, this test shows expected behavior
138
- // In actual implementation, mock would need to be reset properly
139
- });
140
- });
141
-
142
- describe('enable 2FA flow', () => {
143
- it('should show setup view when Enable 2FA is clicked', async () => {
144
- render(<TwoFactorSection />);
145
-
146
- fireEvent.click(screen.getByText('Enable 2FA'));
147
-
148
- await waitFor(() => {
149
- expect(screen.getByTestId('two-factor-setup')).toBeInTheDocument();
150
- });
151
-
152
- expect(mockResetSetup).toHaveBeenCalled();
153
- });
154
-
155
- it('should return to status view when setup is completed', async () => {
156
- render(<TwoFactorSection />);
157
-
158
- // Click Enable 2FA
159
- fireEvent.click(screen.getByText('Enable 2FA'));
160
-
161
- await waitFor(() => {
162
- expect(screen.getByTestId('two-factor-setup')).toBeInTheDocument();
163
- });
164
-
165
- // Complete setup
166
- fireEvent.click(screen.getByText('Complete Setup'));
167
-
168
- await waitFor(() => {
169
- expect(mockFetchStatus).toHaveBeenCalled();
170
- });
171
- });
172
-
173
- it('should return to status view when setup is skipped', async () => {
174
- render(<TwoFactorSection />);
175
-
176
- // Click Enable 2FA
177
- fireEvent.click(screen.getByText('Enable 2FA'));
178
-
179
- await waitFor(() => {
180
- expect(screen.getByTestId('two-factor-setup')).toBeInTheDocument();
181
- });
182
-
183
- // Skip setup
184
- fireEvent.click(screen.getByText('Skip Setup'));
185
-
186
- // Should go back to status view
187
- await waitFor(() => {
188
- expect(screen.queryByTestId('two-factor-setup')).not.toBeInTheDocument();
189
- });
190
- });
191
- });
192
-
193
- describe('error handling', () => {
194
- it('should display error when present', () => {
195
- jest.doMock('@djangocfg/api/auth', () => ({
196
- useTwoFactorStatus: () => ({
197
- isLoading: false,
198
- error: 'Failed to fetch status',
199
- has2FAEnabled: null,
200
- devices: [],
201
- fetchStatus: mockFetchStatus,
202
- disable2FA: mockDisable2FA,
203
- clearError: mockClearError,
204
- }),
205
- useTwoFactorSetup: () => ({
206
- resetSetup: mockResetSetup,
207
- }),
208
- }));
209
-
210
- // Note: Due to jest module caching, mock would need proper reset
211
- });
212
- });
213
-
214
- describe('loading state', () => {
215
- it('should show loading indicator when fetching initial status', () => {
216
- jest.doMock('@djangocfg/api/auth', () => ({
217
- useTwoFactorStatus: () => ({
218
- isLoading: true,
219
- error: null,
220
- has2FAEnabled: null,
221
- devices: [],
222
- fetchStatus: mockFetchStatus,
223
- disable2FA: mockDisable2FA,
224
- clearError: mockClearError,
225
- }),
226
- useTwoFactorSetup: () => ({
227
- resetSetup: mockResetSetup,
228
- }),
229
- }));
230
-
231
- // Note: Due to jest module caching, mock would need proper reset
232
- });
233
- });
234
- });
@@ -1,198 +0,0 @@
1
- // @ts-nocheck
2
- 'use client';
3
-
4
- import React, { useEffect, useState } from 'react';
5
- import { useForm } from 'react-hook-form';
6
- import { toast } from '@djangocfg/ui-core/hooks';
7
-
8
- import {
9
- PatchedCfgUserUpdateRequest, PatchedCfgUserUpdateRequestSchema,
10
- useAuth
11
- } from '@djangocfg/api/auth';
12
- import {
13
- Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, Label, PhoneInput
14
- } from '@djangocfg/ui-core/components';
15
- import { zodResolver } from '@hookform/resolvers/zod';
16
-
17
- import { profileLogger } from '../../../utils/logger';
18
-
19
- export const ProfileForm = () => {
20
- const { user, updateProfile } = useAuth();
21
- const [isEditing, setIsEditing] = useState(false);
22
- const [isSaving, setIsSaving] = useState(false);
23
-
24
- const form = useForm<PatchedCfgUserUpdateRequest>({
25
- resolver: zodResolver(PatchedCfgUserUpdateRequestSchema),
26
- defaultValues: {
27
- first_name: '',
28
- last_name: '',
29
- company: '',
30
- position: '',
31
- phone: '',
32
- },
33
- });
34
-
35
- // Load user data
36
- useEffect(() => {
37
- if (user) {
38
- form.reset({
39
- first_name: user.first_name || '',
40
- last_name: user.last_name || '',
41
- company: user.company || '',
42
- position: user.position || '',
43
- phone: user.phone || '',
44
- });
45
- }
46
- }, [user, form]);
47
-
48
- const handleSubmit = async (data: PatchedCfgUserUpdateRequest) => {
49
- setIsSaving(true);
50
- try {
51
- await updateProfile(data);
52
- toast.success('Profile updated successfully');
53
- setIsEditing(false);
54
- } catch (error: any) {
55
- profileLogger.error('Profile update error:', error);
56
- if (error?.response?.data) {
57
- const fieldErrors = error.response.data;
58
- Object.entries(fieldErrors).forEach(([field, messages]) => {
59
- if (Array.isArray(messages) && messages.length > 0) {
60
- form.setError(field as keyof PatchedCfgUserUpdateRequest, {
61
- type: 'server',
62
- message: messages[0],
63
- });
64
- }
65
- });
66
- toast.error('Please fix the validation errors');
67
- } else {
68
- toast.error('Failed to update profile');
69
- }
70
- } finally {
71
- setIsSaving(false);
72
- }
73
- };
74
-
75
- const handleCancel = () => {
76
- setIsEditing(false);
77
- form.clearErrors();
78
- if (user) {
79
- form.reset({
80
- first_name: user.first_name || '',
81
- last_name: user.last_name || '',
82
- company: user.company || '',
83
- position: user.position || '',
84
- phone: user.phone || '',
85
- });
86
- }
87
- };
88
-
89
- const onSubmit = form.handleSubmit(handleSubmit);
90
-
91
- return (
92
- <Form {...form}>
93
- <form onSubmit={onSubmit} className="space-y-4">
94
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
95
- <div className="space-y-2 md:col-span-2">
96
- <Label htmlFor="email">Email</Label>
97
- <Input id="email" value={user?.email || ''} disabled className="bg-muted" />
98
- </div>
99
-
100
- <FormField
101
- control={form.control}
102
- name="first_name"
103
- render={({ field }) => (
104
- <FormItem>
105
- <FormLabel>First Name</FormLabel>
106
- <FormControl>
107
- <Input {...field} disabled={!isEditing} placeholder="Enter first name" />
108
- </FormControl>
109
- <FormMessage />
110
- </FormItem>
111
- )}
112
- />
113
-
114
- <FormField
115
- control={form.control}
116
- name="last_name"
117
- render={({ field }) => (
118
- <FormItem>
119
- <FormLabel>Last Name</FormLabel>
120
- <FormControl>
121
- <Input {...field} disabled={!isEditing} placeholder="Enter last name" />
122
- </FormControl>
123
- <FormMessage />
124
- </FormItem>
125
- )}
126
- />
127
-
128
- <FormField
129
- control={form.control}
130
- name="company"
131
- render={({ field }) => (
132
- <FormItem>
133
- <FormLabel>Company</FormLabel>
134
- <FormControl>
135
- <Input {...field} disabled={!isEditing} placeholder="Enter company name" />
136
- </FormControl>
137
- <FormMessage />
138
- </FormItem>
139
- )}
140
- />
141
-
142
- <FormField
143
- control={form.control}
144
- name="position"
145
- render={({ field }) => (
146
- <FormItem>
147
- <FormLabel>Position</FormLabel>
148
- <FormControl>
149
- <Input {...field} disabled={!isEditing} placeholder="Enter position" />
150
- </FormControl>
151
- <FormMessage />
152
- </FormItem>
153
- )}
154
- />
155
-
156
- <FormField
157
- control={form.control}
158
- name="phone"
159
- render={({ field }) => (
160
- <FormItem className="md:col-span-2">
161
- <FormLabel>Phone</FormLabel>
162
- <FormControl>
163
- <PhoneInput
164
- value={field.value}
165
- onChange={field.onChange}
166
- disabled={!isEditing}
167
- placeholder="Enter phone number"
168
- defaultCountry="US"
169
- />
170
- </FormControl>
171
- <FormMessage />
172
- </FormItem>
173
- )}
174
- />
175
- </div>
176
-
177
- {/* Action Buttons */}
178
- <div className="flex items-center justify-between pt-4">
179
- {isEditing ? (
180
- <div className="flex items-center gap-2">
181
- <Button type="submit" disabled={isSaving}>
182
- {isSaving ? 'Saving...' : 'Save Changes'}
183
- </Button>
184
- <Button type="button" variant="outline" onClick={handleCancel}>
185
- Cancel
186
- </Button>
187
- </div>
188
- ) : (
189
- <Button type="button" onClick={() => setIsEditing(true)}>
190
- Edit Profile
191
- </Button>
192
- )}
193
- </div>
194
- </form>
195
- </Form>
196
- );
197
- };
198
-