@open-kingdom/shared-frontend-feature-user-management 0.0.2-13 → 0.0.2-16

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 (42) hide show
  1. package/README.md +136 -4
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +293 -167
  5. package/dist/lib/components/invitations/InvitationList.component.d.ts +2 -0
  6. package/dist/lib/components/invitations/InvitationList.component.d.ts.map +1 -0
  7. package/dist/lib/components/invitations/index.d.ts +1 -0
  8. package/dist/lib/components/invitations/index.d.ts.map +1 -1
  9. package/dist/lib/components/shared/StatusBadge.component.d.ts +7 -0
  10. package/dist/lib/components/shared/StatusBadge.component.d.ts.map +1 -0
  11. package/dist/lib/components/users/UserList.component.d.ts.map +1 -1
  12. package/dist/lib/styles.d.ts +1 -0
  13. package/dist/lib/styles.d.ts.map +1 -1
  14. package/dist/lib/types.d.ts +10 -0
  15. package/dist/lib/types.d.ts.map +1 -1
  16. package/package.json +6 -1
  17. package/.babelrc +0 -12
  18. package/jest.config.cts +0 -14
  19. package/src/index.ts +0 -4
  20. package/src/lib/components/invitations/AcceptInvitation.component.spec.tsx +0 -154
  21. package/src/lib/components/invitations/AcceptInvitation.component.tsx +0 -197
  22. package/src/lib/components/invitations/InviteUserModal.component.spec.tsx +0 -79
  23. package/src/lib/components/invitations/InviteUserModal.component.tsx +0 -121
  24. package/src/lib/components/invitations/index.ts +0 -2
  25. package/src/lib/components/shared/ConfirmDialog.component.spec.tsx +0 -45
  26. package/src/lib/components/shared/ConfirmDialog.component.tsx +0 -58
  27. package/src/lib/components/shared/FormField.component.spec.tsx +0 -50
  28. package/src/lib/components/shared/FormField.component.tsx +0 -34
  29. package/src/lib/components/shared/ModalOverlay.component.spec.tsx +0 -81
  30. package/src/lib/components/shared/ModalOverlay.component.tsx +0 -45
  31. package/src/lib/components/shared/RoleBadge.component.spec.tsx +0 -20
  32. package/src/lib/components/shared/RoleBadge.component.tsx +0 -25
  33. package/src/lib/components/shared/StatusCard.component.spec.tsx +0 -44
  34. package/src/lib/components/shared/StatusCard.component.tsx +0 -47
  35. package/src/lib/components/users/UserList.component.spec.tsx +0 -216
  36. package/src/lib/components/users/UserList.component.tsx +0 -153
  37. package/src/lib/styles.ts +0 -19
  38. package/src/lib/types.ts +0 -9
  39. package/tsconfig.json +0 -13
  40. package/tsconfig.lib.json +0 -47
  41. package/tsconfig.spec.json +0 -27
  42. package/vite.config.mts +0 -58
@@ -0,0 +1,2 @@
1
+ export declare function InvitationList(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=InvitationList.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvitationList.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/invitations/InvitationList.component.tsx"],"names":[],"mappings":"AA2FA,wBAAgB,cAAc,4CA8E7B"}
@@ -1,3 +1,4 @@
1
1
  export { InviteUserModal } from './InviteUserModal.component';
2
2
  export { AcceptInvitation } from './AcceptInvitation.component';
3
+ export { InvitationList } from './InvitationList.component';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/invitations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/invitations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { InvitationStatus } from '../../types';
2
+ interface StatusBadgeProps {
3
+ status: InvitationStatus;
4
+ }
5
+ export declare function StatusBadge({ status }: StatusBadgeProps): import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
7
+ //# sourceMappingURL=StatusBadge.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusBadge.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/shared/StatusBadge.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAWpD,UAAU,gBAAgB;IACxB,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,gBAAgB,kDAUvD"}
@@ -1 +1 @@
1
- {"version":3,"file":"UserList.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/users/UserList.component.tsx"],"names":[],"mappings":"AAmBA,UAAU,aAAa;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,QAAQ,CAAC,EAAE,aAAa,EAAE,EAAE,aAAa,2CAiIxD"}
1
+ {"version":3,"file":"UserList.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/users/UserList.component.tsx"],"names":[],"mappings":"AAoBA,UAAU,aAAa;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,QAAQ,CAAC,EAAE,aAAa,EAAE,EAAE,aAAa,2CAiIxD"}
@@ -5,4 +5,5 @@ export declare const bodyTextStyles = "text-neutral-600 dark:text-neutral-400";
5
5
  export declare const buttonPrimaryStyles = "rounded-md px-4 py-2 text-sm font-medium transition-colors bg-primary-500 text-white hover:bg-primary-600 disabled:opacity-50";
6
6
  export declare const buttonSecondaryStyles = "rounded-md px-4 py-2 text-sm font-medium transition-colors border border-neutral-300 text-neutral-700 hover:bg-neutral-50 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-700";
7
7
  export declare const buttonDangerStyles = "rounded-md px-4 py-2 text-sm font-medium transition-colors bg-error-500 text-white hover:bg-error-600 disabled:opacity-50";
8
+ export declare const buttonInlineDestructiveStyles = "rounded px-2 py-1 text-xs font-medium text-error-600 transition-colors hover:bg-error-50 disabled:cursor-not-allowed disabled:opacity-40 dark:text-error-400 dark:hover:bg-error-900/20";
8
9
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/lib/styles.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,sJAC6H,CAAC;AAEtJ,eAAO,MAAM,WAAW,0EACiD,CAAC;AAE1E,eAAO,MAAM,UAAU,gFACwD,CAAC;AAEhF,eAAO,MAAM,cAAc,2CAA2C,CAAC;AAKvE,eAAO,MAAM,mBAAmB,kIAA2F,CAAC;AAE5H,eAAO,MAAM,qBAAqB,sMAA+J,CAAC;AAElM,eAAO,MAAM,kBAAkB,8HAAuF,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/lib/styles.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,sJAC6H,CAAC;AAEtJ,eAAO,MAAM,WAAW,0EACiD,CAAC;AAE1E,eAAO,MAAM,UAAU,gFACwD,CAAC;AAEhF,eAAO,MAAM,cAAc,2CAA2C,CAAC;AAKvE,eAAO,MAAM,mBAAmB,kIAA2F,CAAC;AAE5H,eAAO,MAAM,qBAAqB,sMAA+J,CAAC;AAElM,eAAO,MAAM,kBAAkB,8HAAuF,CAAC;AAEvH,eAAO,MAAM,6BAA6B,4LACiJ,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export type Role = 'guest' | 'user' | 'admin';
2
+ export type InvitationStatus = 'pending' | 'accepted' | 'expired';
2
3
  export interface User {
3
4
  id: number;
4
5
  email: string;
@@ -6,4 +7,13 @@ export interface User {
6
7
  lastName: string | null;
7
8
  role: Role;
8
9
  }
10
+ export interface Invitation {
11
+ id: number;
12
+ email: string;
13
+ tokenExpiry: number;
14
+ invitedBy: number;
15
+ invitedAt: number;
16
+ role: Role;
17
+ status: InvitationStatus;
18
+ }
9
19
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,IAAI,CAAC;CACZ"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,gBAAgB,CAAC;CAC1B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-kingdom/shared-frontend-feature-user-management",
3
- "version": "0.0.2-13",
3
+ "version": "0.0.2-16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -17,6 +17,11 @@
17
17
  "default": "./dist/index.js"
18
18
  }
19
19
  },
20
+ "files": [
21
+ "README.md",
22
+ "dist",
23
+ "!**/*.tsbuildinfo"
24
+ ],
20
25
  "nx": {
21
26
  "tags": [
22
27
  "scope:shared",
package/.babelrc DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "presets": [
3
- [
4
- "@nx/react/babel",
5
- {
6
- "runtime": "automatic",
7
- "useBuiltIns": "usage"
8
- }
9
- ]
10
- ],
11
- "plugins": []
12
- }
package/jest.config.cts DELETED
@@ -1,14 +0,0 @@
1
- module.exports = {
2
- displayName: '@open-kingdom/feature-user-management',
3
- preset: '../../../../jest.preset.js',
4
- transform: {
5
- '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
6
- '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
7
- },
8
- transformIgnorePatterns: [
9
- 'node_modules/(?!(@react-hookz/web|@ver0/deep-equal)/)',
10
- ],
11
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
12
- coverageDirectory: 'test-output/jest/coverage',
13
- coveragePathIgnorePatterns: ['src/lib/types.ts'],
14
- };
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { UserList } from './lib/components/users/UserList.component';
2
- export { AcceptInvitation } from './lib/components/invitations';
3
- export { StatusCard } from './lib/components/shared/StatusCard.component';
4
- export type { User, Role } from './lib/types';
@@ -1,154 +0,0 @@
1
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
- import '@testing-library/jest-dom';
3
- import { AcceptInvitation } from './AcceptInvitation.component';
4
-
5
- const mockValidateQuery = jest.fn();
6
- const mockAccept = jest.fn();
7
- const mockAcceptState = jest.fn();
8
-
9
- jest.mock('@open-kingdom/shared-frontend-data-access-api-client', () => ({
10
- useInvitationsControllerValidateQuery: (...args: unknown[]) =>
11
- mockValidateQuery(...args),
12
- useInvitationsControllerAcceptMutation: () => [mockAccept, mockAcceptState()],
13
- }));
14
-
15
- describe('AcceptInvitation', () => {
16
- beforeEach(() => {
17
- mockAccept.mockReset();
18
- mockAccept.mockReturnValue({ unwrap: () => Promise.resolve() });
19
- mockAcceptState.mockReturnValue({ isLoading: false, isSuccess: false });
20
- });
21
-
22
- it('shows a loading message while checking the invitation', () => {
23
- mockValidateQuery.mockReturnValue({
24
- data: undefined,
25
- isLoading: true,
26
- error: null,
27
- });
28
- render(<AcceptInvitation token="test-token" />);
29
- expect(screen.getByText('Validating invitation...')).toBeInTheDocument();
30
- });
31
-
32
- it('tells the user when something went wrong checking the invitation', () => {
33
- mockValidateQuery.mockReturnValue({
34
- data: undefined,
35
- isLoading: false,
36
- error: { status: 500 },
37
- });
38
- render(<AcceptInvitation token="bad-token" />);
39
- expect(screen.getByText('Validation Failed')).toBeInTheDocument();
40
- });
41
-
42
- it('tells the user when the invitation link is invalid or expired', () => {
43
- mockValidateQuery.mockReturnValue({
44
- data: { valid: false },
45
- isLoading: false,
46
- error: null,
47
- });
48
- render(<AcceptInvitation token="bad-token" />);
49
- expect(screen.getByText('Invalid Invitation')).toBeInTheDocument();
50
- });
51
-
52
- it('shows the registration form for a valid invitation', () => {
53
- mockValidateQuery.mockReturnValue({
54
- data: { valid: true, email: 'test@example.com', role: 'user' },
55
- isLoading: false,
56
- error: null,
57
- });
58
- render(<AcceptInvitation token="test-token" />);
59
- expect(screen.getByText('Accept Invitation')).toBeInTheDocument();
60
- expect(screen.getByLabelText(/first name/i)).toBeInTheDocument();
61
- expect(screen.getByLabelText(/last name/i)).toBeInTheDocument();
62
- expect(screen.getByLabelText(/^password/i)).toBeInTheDocument();
63
- expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument();
64
- });
65
-
66
- it('shows the invited email and assigned role', () => {
67
- mockValidateQuery.mockReturnValue({
68
- data: { valid: true, email: 'test@example.com', role: 'admin' },
69
- isLoading: false,
70
- error: null,
71
- });
72
- render(<AcceptInvitation token="test-token" />);
73
- expect(screen.getByText('admin')).toBeInTheDocument();
74
- expect(screen.getByText('test@example.com')).toBeInTheDocument();
75
- });
76
-
77
- it('requires a password of at least 8 characters', async () => {
78
- mockValidateQuery.mockReturnValue({
79
- data: { valid: true, email: 'test@example.com', role: 'user' },
80
- isLoading: false,
81
- error: null,
82
- });
83
- render(<AcceptInvitation token="test-token" />);
84
- fireEvent.change(screen.getByLabelText(/^password/i), {
85
- target: { value: 'short' },
86
- });
87
- fireEvent.change(screen.getByLabelText(/confirm password/i), {
88
- target: { value: 'short' },
89
- });
90
- fireEvent.click(screen.getByText('Create Account'));
91
- await waitFor(() => {
92
- expect(
93
- screen.getByText('Password must be at least 8 characters')
94
- ).toBeInTheDocument();
95
- });
96
- });
97
-
98
- it('requires password and confirmation to match', async () => {
99
- mockValidateQuery.mockReturnValue({
100
- data: { valid: true, email: 'test@example.com', role: 'user' },
101
- isLoading: false,
102
- error: null,
103
- });
104
- render(<AcceptInvitation token="test-token" />);
105
- fireEvent.change(screen.getByLabelText(/^password/i), {
106
- target: { value: 'password123' },
107
- });
108
- fireEvent.change(screen.getByLabelText(/confirm password/i), {
109
- target: { value: 'different123' },
110
- });
111
- fireEvent.click(screen.getByText('Create Account'));
112
- await waitFor(() => {
113
- expect(screen.getByText('Passwords do not match')).toBeInTheDocument();
114
- });
115
- });
116
-
117
- it('submits the account details when the form is filled correctly', async () => {
118
- mockValidateQuery.mockReturnValue({
119
- data: { valid: true, email: 'test@example.com', role: 'user' },
120
- isLoading: false,
121
- error: null,
122
- });
123
- render(<AcceptInvitation token="good-token" />);
124
- fireEvent.change(screen.getByLabelText(/^password/i), {
125
- target: { value: 'password123' },
126
- });
127
- fireEvent.change(screen.getByLabelText(/confirm password/i), {
128
- target: { value: 'password123' },
129
- });
130
- fireEvent.click(screen.getByText('Create Account'));
131
- await waitFor(() => {
132
- expect(mockAccept).toHaveBeenCalledWith({
133
- acceptInvitationDto: {
134
- token: 'good-token',
135
- password: 'password123',
136
- firstName: undefined,
137
- lastName: undefined,
138
- },
139
- });
140
- });
141
- });
142
-
143
- it('confirms account creation and links to login', () => {
144
- mockValidateQuery.mockReturnValue({
145
- data: { valid: true, email: 'test@example.com', role: 'user' },
146
- isLoading: false,
147
- error: null,
148
- });
149
- mockAcceptState.mockReturnValue({ isLoading: false, isSuccess: true });
150
- render(<AcceptInvitation token="good-token" loginPath="/profile" />);
151
- expect(screen.getByText('Account Created')).toBeInTheDocument();
152
- expect(screen.getByText('Go to login')).toHaveAttribute('href', '/profile');
153
- });
154
- });
@@ -1,197 +0,0 @@
1
- import { useForm } from 'react-hook-form';
2
- import { zodResolver } from '@hookform/resolvers/zod';
3
- import { z } from 'zod';
4
- import {
5
- useInvitationsControllerValidateQuery,
6
- useInvitationsControllerAcceptMutation,
7
- } from '@open-kingdom/shared-frontend-data-access-api-client';
8
- import { StatusCard } from '../shared/StatusCard.component';
9
- import { FormField } from '../shared/FormField.component';
10
- import {
11
- inputStyles,
12
- buttonPrimaryStyles,
13
- cardStyles,
14
- bodyTextStyles,
15
- } from '../../styles';
16
-
17
- const acceptSchema = z
18
- .object({
19
- firstName: z.string().optional(),
20
- lastName: z.string().optional(),
21
- password: z.string().min(8, 'Password must be at least 8 characters'),
22
- confirmPassword: z.string(),
23
- })
24
- .refine((data) => data.password === data.confirmPassword, {
25
- message: 'Passwords do not match',
26
- path: ['confirmPassword'],
27
- });
28
-
29
- type AcceptFormValues = z.infer<typeof acceptSchema>;
30
-
31
- interface AcceptInvitationProps {
32
- token: string;
33
- loginPath?: string;
34
- }
35
-
36
- export function AcceptInvitation({ token, loginPath }: AcceptInvitationProps) {
37
- const {
38
- data: validation,
39
- isLoading: isValidating,
40
- error: validationError,
41
- } = useInvitationsControllerValidateQuery({ token });
42
-
43
- const [accept, { isLoading, isSuccess }] =
44
- useInvitationsControllerAcceptMutation();
45
-
46
- const {
47
- register,
48
- handleSubmit,
49
- formState: { errors },
50
- } = useForm<AcceptFormValues>({
51
- resolver: zodResolver(acceptSchema),
52
- defaultValues: {
53
- firstName: '',
54
- lastName: '',
55
- password: '',
56
- confirmPassword: '',
57
- },
58
- });
59
-
60
- const email = validation?.email ?? '';
61
- const role = validation?.role ?? 'user';
62
-
63
- const onSubmit = async (data: AcceptFormValues) => {
64
- try {
65
- await accept({
66
- acceptInvitationDto: {
67
- token,
68
- password: data.password,
69
- firstName: data.firstName || undefined,
70
- lastName: data.lastName || undefined,
71
- },
72
- }).unwrap();
73
- } catch {
74
- // Error notification handled by RTK error middleware
75
- }
76
- };
77
-
78
- if (isSuccess) {
79
- return (
80
- <StatusCard
81
- variant="success"
82
- title="Account Created"
83
- message="Your account has been created successfully. You can now log in with your email and password."
84
- >
85
- {loginPath && (
86
- <a
87
- href={loginPath}
88
- data-testid="accept-login-link"
89
- className="mt-4 inline-block text-sm font-medium text-primary-600 hover:underline dark:text-primary-400"
90
- >
91
- Go to login
92
- </a>
93
- )}
94
- </StatusCard>
95
- );
96
- }
97
-
98
- if (isValidating) {
99
- return <StatusCard variant="loading" message="Validating invitation..." />;
100
- }
101
-
102
- if (validationError) {
103
- return (
104
- <StatusCard
105
- variant="error"
106
- title="Validation Failed"
107
- message="Unable to validate this invitation. Please check your connection and try again."
108
- />
109
- );
110
- }
111
-
112
- if (!validation?.valid) {
113
- return (
114
- <StatusCard
115
- variant="error"
116
- title="Invalid Invitation"
117
- message="This invitation link is invalid or has expired. Please contact the person who invited you for a new link."
118
- />
119
- );
120
- }
121
-
122
- return (
123
- <div className={cardStyles}>
124
- <h2
125
- data-testid="accept-heading"
126
- className="text-xl font-bold text-neutral-900 dark:text-neutral-100"
127
- >
128
- Accept Invitation
129
- </h2>
130
- <p className={`mt-1 text-sm ${bodyTextStyles}`}>
131
- You've been invited as <strong data-testid="accept-role">{role}</strong>{' '}
132
- with email <strong data-testid="accept-email">{email}</strong>
133
- </p>
134
-
135
- <form onSubmit={handleSubmit(onSubmit)} className="mt-4 space-y-4">
136
- <FormField label="First Name" htmlFor="accept-firstName">
137
- <input
138
- id="accept-firstName"
139
- data-testid="accept-first-name-input"
140
- type="text"
141
- placeholder="John"
142
- className={inputStyles}
143
- {...register('firstName')}
144
- />
145
- </FormField>
146
- <FormField label="Last Name" htmlFor="accept-lastName">
147
- <input
148
- id="accept-lastName"
149
- data-testid="accept-last-name-input"
150
- type="text"
151
- placeholder="Doe"
152
- className={inputStyles}
153
- {...register('lastName')}
154
- />
155
- </FormField>
156
- <FormField
157
- label="Password"
158
- htmlFor="accept-password"
159
- required
160
- error={errors.password?.message}
161
- >
162
- <input
163
- id="accept-password"
164
- data-testid="accept-password-input"
165
- type="password"
166
- placeholder="Min. 8 characters"
167
- className={inputStyles}
168
- {...register('password')}
169
- />
170
- </FormField>
171
- <FormField
172
- label="Confirm Password"
173
- htmlFor="accept-confirmPassword"
174
- required
175
- error={errors.confirmPassword?.message}
176
- >
177
- <input
178
- id="accept-confirmPassword"
179
- data-testid="accept-confirm-password-input"
180
- type="password"
181
- placeholder="Repeat password"
182
- className={inputStyles}
183
- {...register('confirmPassword')}
184
- />
185
- </FormField>
186
- <button
187
- type="submit"
188
- data-testid="accept-submit-btn"
189
- disabled={isLoading}
190
- className={`w-full ${buttonPrimaryStyles}`}
191
- >
192
- {isLoading ? 'Creating account...' : 'Create Account'}
193
- </button>
194
- </form>
195
- </div>
196
- );
197
- }
@@ -1,79 +0,0 @@
1
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
- import '@testing-library/jest-dom';
3
- import { Provider } from 'react-redux';
4
- import { configureStore } from '@reduxjs/toolkit';
5
- import { InviteUserModal } from './InviteUserModal.component';
6
-
7
- const mockInvite = jest.fn();
8
-
9
- jest.mock('@open-kingdom/shared-frontend-data-access-api-client', () => ({
10
- useInvitationsControllerInviteMutation: () => [
11
- mockInvite,
12
- { isLoading: false },
13
- ],
14
- }));
15
-
16
- jest.mock('@open-kingdom/shared-frontend-data-access-notifications', () => ({
17
- showSuccessNotification: jest.fn((msg: string) => ({
18
- type: 'notify',
19
- payload: msg,
20
- })),
21
- }));
22
-
23
- const store = configureStore({ reducer: {} });
24
-
25
- function renderWithProviders(ui: React.ReactElement) {
26
- return render(<Provider store={store}>{ui}</Provider>);
27
- }
28
-
29
- describe('InviteUserModal', () => {
30
- beforeEach(() => {
31
- mockInvite.mockReset();
32
- mockInvite.mockReturnValue({ unwrap: () => Promise.resolve() });
33
- });
34
-
35
- it('shows the invitation form when opened', () => {
36
- renderWithProviders(<InviteUserModal isOpen={true} onClose={jest.fn()} />);
37
- expect(screen.getByText('Invite User')).toBeInTheDocument();
38
- expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
39
- expect(screen.getByLabelText(/role/i)).toBeInTheDocument();
40
- });
41
-
42
- it('hides the form when closed', () => {
43
- renderWithProviders(<InviteUserModal isOpen={false} onClose={jest.fn()} />);
44
- expect(screen.queryByText('Invite User')).not.toBeInTheDocument();
45
- });
46
-
47
- it('sends the invitation with the entered email and role', async () => {
48
- const onClose = jest.fn();
49
- renderWithProviders(<InviteUserModal isOpen={true} onClose={onClose} />);
50
- fireEvent.change(screen.getByLabelText(/email/i), {
51
- target: { value: 'user@example.com' },
52
- });
53
- fireEvent.click(screen.getByText('Send Invitation'));
54
- await waitFor(() => {
55
- expect(mockInvite).toHaveBeenCalledWith({
56
- inviteUserDto: { email: 'user@example.com', role: 'guest' },
57
- });
58
- });
59
- });
60
-
61
- it('closes after sending the invitation', async () => {
62
- const onClose = jest.fn();
63
- renderWithProviders(<InviteUserModal isOpen={true} onClose={onClose} />);
64
- fireEvent.change(screen.getByLabelText(/email/i), {
65
- target: { value: 'user@example.com' },
66
- });
67
- fireEvent.click(screen.getByText('Send Invitation'));
68
- await waitFor(() => {
69
- expect(onClose).toHaveBeenCalled();
70
- });
71
- });
72
-
73
- it('closes when clicking Cancel', () => {
74
- const onClose = jest.fn();
75
- renderWithProviders(<InviteUserModal isOpen={true} onClose={onClose} />);
76
- fireEvent.click(screen.getByText('Cancel'));
77
- expect(onClose).toHaveBeenCalled();
78
- });
79
- });