form-craft-package 1.7.9-dev.0 → 1.7.9-dev.2

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 (54) hide show
  1. package/package.json +3 -2
  2. package/src/api/client.ts +1 -1
  3. package/src/api/user.ts +7 -8
  4. package/src/components/common/custom-hooks/use-breadcrumb.hook.ts +90 -0
  5. package/src/components/common/custom-hooks/use-check-element-conditions.hook.ts +3 -3
  6. package/src/components/common/custom-hooks/use-find-dynamic-form.hook.ts +30 -11
  7. package/src/components/common/custom-hooks/use-login-handler.ts +81 -4
  8. package/src/components/common/custom-hooks/use-many-to-many-connector.hook.ts +30 -0
  9. package/src/components/common/not-found.tsx +21 -0
  10. package/src/components/common/section-panel.tsx +42 -0
  11. package/src/components/companies/1-authenticated/change-password.tsx +110 -0
  12. package/src/components/companies/2-unauthenticated/reset-password.tsx +128 -0
  13. package/src/components/companies/index.tsx +2 -0
  14. package/src/components/form/1-list/index.tsx +37 -38
  15. package/src/components/form/1-list/table-header.tsx +6 -6
  16. package/src/components/form/1-list/table.tsx +10 -12
  17. package/src/components/form/2-details/index.tsx +63 -41
  18. package/src/components/form/layout-renderer/1-row/index.tsx +12 -5
  19. package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +59 -89
  20. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-navigate.hook.tsx +88 -0
  21. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-create-data.hook.ts +22 -23
  22. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +3 -4
  23. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-save-data.hook.ts +2 -2
  24. package/src/components/form/layout-renderer/3-element/11-breadcrumb.tsx +30 -0
  25. package/src/components/form/layout-renderer/3-element/2-field-element.tsx +4 -1
  26. package/src/components/form/layout-renderer/3-element/4-rich-text-editor.tsx +27 -2
  27. package/src/components/form/layout-renderer/3-element/6-signature.tsx +5 -1
  28. package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +115 -74
  29. package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +50 -110
  30. package/src/components/form/layout-renderer/3-element/index.tsx +27 -9
  31. package/src/components/modals/report-filters.modal/helper-functions.ts +3 -6
  32. package/src/constants.ts +62 -45
  33. package/src/enums/form.enum.ts +5 -3
  34. package/src/enums/index.ts +9 -3
  35. package/src/{global-helpers → functions}/cookie-handler.ts +9 -10
  36. package/src/functions/forms/breadcrumb-handlers.ts +21 -0
  37. package/src/functions/forms/create-form-rules.ts +4 -1
  38. package/src/functions/forms/data-render-functions.tsx +5 -4
  39. package/src/functions/forms/extended-json-handlers.ts +56 -0
  40. package/src/functions/forms/index.ts +17 -11
  41. package/src/functions/index.ts +2 -1
  42. package/src/functions/reports/index.tsx +2 -1
  43. package/src/types/companies/site-layout/authenticated/index.tsx +23 -14
  44. package/src/types/forms/data-list/index.ts +0 -7
  45. package/src/types/forms/index.ts +1 -0
  46. package/src/types/forms/layout-elements/button.ts +11 -3
  47. package/src/types/forms/layout-elements/data-render-config.ts +1 -0
  48. package/src/types/forms/layout-elements/index.ts +12 -2
  49. package/src/types/forms/layout-elements/sanitization.ts +6 -1
  50. package/src/types/forms/relationship/index.ts +12 -1
  51. package/src/types/index.ts +2 -0
  52. package/src/functions/forms/json-handlers.ts +0 -19
  53. package/src/global-helpers/constants.ts +0 -2
  54. package/src/global-helpers/enums.ts +0 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.7.9-dev.0",
3
+ "version": "1.7.9-dev.2",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -12,8 +12,9 @@
12
12
  "description": "",
13
13
  "dependencies": {
14
14
  "@types/react-google-recaptcha": "^2.1.9",
15
- "@types/react-signature-canvas": "^1.0.6",
15
+ "@types/react-signature-canvas": "^1.0.7",
16
16
  "ajv": "^8.17.1",
17
+ "bson": "^4.7.2",
17
18
  "pdfmake": "^0.2.18",
18
19
  "qs": "^6.14.0",
19
20
  "react-google-recaptcha": "^3.1.0",
package/src/api/client.ts CHANGED
@@ -6,7 +6,7 @@ import { LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS } from '../enums'
6
6
 
7
7
  import { IDynamicForm } from '../types'
8
8
  import { constructDynamicFormHref, fetchDynamicForms } from '../functions'
9
- import { CLIENT_ID, CLIENT_SECRET } from '../global-helpers/constants'
9
+ import { CLIENT_ID, CLIENT_SECRET } from '../constants'
10
10
 
11
11
  const baseURL = import.meta.env.VITE_API_BASE_URL
12
12
 
package/src/api/user.ts CHANGED
@@ -1,15 +1,14 @@
1
- import { SHARED_COOKIE_KEYS } from "../enums"
2
- import { CLIENT_SECRET } from "../global-helpers/constants"
3
- import { cookieHandler } from "../global-helpers/cookie-handler"
1
+ import { SHARED_COOKIE_KEYS } from '../enums'
2
+ import { cookieHandler } from '../functions/cookie-handler'
4
3
  import crypto from 'crypto-js'
5
- import { CookieKeysEnum } from "../global-helpers/enums"
4
+ import { CLIENT_SECRET } from '../constants'
6
5
 
7
6
  const endSession = () => {
8
7
  cookieHandler.empty()
9
8
  localStorage.clear()
10
9
  return
11
10
  }
12
- const decrypt = (toDecrypt: CookieKeysEnum) => {
11
+ const decrypt = (toDecrypt: SHARED_COOKIE_KEYS) => {
13
12
  const cookieValue = cookieHandler.get(toDecrypt)
14
13
  if (!cookieValue) return ''
15
14
  const bytes = crypto.AES.decrypt(cookieValue, CLIENT_SECRET)
@@ -31,19 +30,19 @@ export const UserAuth = {
31
30
  },
32
31
  getUserEmail: () => {
33
32
  try {
34
- return decrypt(CookieKeysEnum.UserEmail) ? decrypt(CookieKeysEnum.UserEmail) : ''
33
+ return decrypt(SHARED_COOKIE_KEYS.UserEmail) ? decrypt(SHARED_COOKIE_KEYS.UserEmail) : ''
35
34
  } catch {
36
35
  endSession()
37
36
  }
38
37
  },
39
38
  getUserFullName: () => {
40
39
  try {
41
- return decrypt(CookieKeysEnum.FullName) ? decrypt(CookieKeysEnum.FullName) : ''
40
+ return decrypt(SHARED_COOKIE_KEYS.FullName) ? decrypt(SHARED_COOKIE_KEYS.FullName) : ''
42
41
  } catch {
43
42
  endSession()
44
43
  }
45
44
  },
46
45
  getUserId: () => {
47
- return cookieHandler.get(CookieKeysEnum.UserId)
46
+ return cookieHandler.get(SHARED_COOKIE_KEYS.UserId)
48
47
  },
49
48
  }
@@ -0,0 +1,90 @@
1
+ import { useSyncExternalStore } from 'react'
2
+ import { LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
3
+ import { NEW_FORM_DATA_IDENTIFIER } from '../../../constants'
4
+
5
+ export type IBreadcrumb = { label: string; href: string; [key: string]: any }
6
+
7
+ let crumbs: IBreadcrumb[] = loadFromStorage()
8
+ const listeners = new Set<() => void>()
9
+
10
+ function loadFromStorage(): IBreadcrumb[] {
11
+ try {
12
+ const raw = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.Breadcrumb)
13
+ return raw ? JSON.parse(raw) : []
14
+ } catch {
15
+ return []
16
+ }
17
+ }
18
+
19
+ function saveToStorage() {
20
+ try {
21
+ localStorage.setItem(LOCAL_STORAGE_KEYS_ENUM.Breadcrumb, JSON.stringify(crumbs))
22
+ } catch {}
23
+ }
24
+
25
+ function notify() {
26
+ for (const fn of listeners) fn()
27
+ }
28
+
29
+ function handlePopState() {
30
+ const href = window.location.pathname
31
+ const idx = crumbs.findIndex((c) => c.href === href)
32
+ breadcrumbStore.sliceAt(idx >= 0 ? idx : 0)
33
+ }
34
+
35
+ if (typeof window !== 'undefined') {
36
+ window.addEventListener('popstate', handlePopState)
37
+ }
38
+
39
+ export const breadcrumbStore = {
40
+ getCrumbs: () => crumbs,
41
+ reset: () => {
42
+ crumbs = []
43
+ saveToStorage()
44
+ notify()
45
+ },
46
+ push: (c: IBreadcrumb) => {
47
+ const last = crumbs[crumbs.length - 1]
48
+ if (last?.href === c.href) return
49
+
50
+ if (last?.href.endsWith(NEW_FORM_DATA_IDENTIFIER)) crumbs = [...crumbs.slice(0, -1), c]
51
+ else crumbs = [...crumbs, c]
52
+
53
+ saveToStorage()
54
+ notify()
55
+ },
56
+ updateCrumb: (href: string, props: Partial<IBreadcrumb>): void => {
57
+ let changed = false
58
+ crumbs = crumbs.map((c) => {
59
+ if (c.href === href) {
60
+ changed = true
61
+ return { ...c, ...props }
62
+ }
63
+ return c
64
+ })
65
+
66
+ if (changed) {
67
+ saveToStorage()
68
+ notify()
69
+ }
70
+ },
71
+ sliceAt: (i: number) => {
72
+ crumbs = crumbs.slice(0, i + 1)
73
+ saveToStorage()
74
+ notify()
75
+ },
76
+ subscribe: (fn: () => void) => {
77
+ listeners.add(fn)
78
+ return () => void listeners.delete(fn)
79
+ },
80
+ }
81
+
82
+ export function useBreadcrumb() {
83
+ return {
84
+ breadcrumbs: useSyncExternalStore(breadcrumbStore.subscribe, breadcrumbStore.getCrumbs),
85
+ reset: breadcrumbStore.reset,
86
+ push: breadcrumbStore.push,
87
+ sliceAt: breadcrumbStore.sliceAt,
88
+ updateCrumb: breadcrumbStore.updateCrumb,
89
+ }
90
+ }
@@ -23,7 +23,7 @@ export const useCheckElementConditions = ({
23
23
  const currentBreakpoint = useGetCurrentBreakpoint()
24
24
 
25
25
  const formValues = Form.useWatch([], { form: formRef, preserve: true })
26
-
26
+
27
27
  useEffect(() => {
28
28
  if (!!formValues) {
29
29
  setIsDisabled(
@@ -39,7 +39,7 @@ export const useCheckElementConditions = ({
39
39
  }),
40
40
  )
41
41
  }
42
- }, [formValues, conditions])
42
+ }, [formValues, conditions, formContext, currentBreakpoint])
43
43
 
44
44
  return { isElementDisabled: isDisabled, isElementHidden: isHidden }
45
45
  }
@@ -71,7 +71,7 @@ const isValidationsMet = (
71
71
  // Dependent on data id
72
72
  const isDataIdConditionsMet = hasDataIdValidation
73
73
  ? hasDataIdValidation.state === FormStateEnum.New
74
- ? contextValues.formDataId === NEW_FORM_DATA_IDENTIFIER
74
+ ? !contextValues.formDataId || contextValues.formDataId === NEW_FORM_DATA_IDENTIFIER
75
75
  : contextValues.formDataId !== NEW_FORM_DATA_IDENTIFIER
76
76
  : true
77
77
 
@@ -1,27 +1,46 @@
1
- import { useState, useEffect } from 'react'
1
+ import { useState, useEffect, useCallback } from 'react'
2
2
  import { useParams } from 'react-router-dom'
3
3
  import { IDynamicForm } from '../../../types'
4
4
  import { LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
5
5
 
6
6
  export const useFindDynamiForm = () => {
7
7
  const { formName } = useParams<{ formName: string }>()
8
- const [foundItem, setFoundItem] = useState<IDynamicForm | undefined | null>(undefined)
8
+ const [foundItem, setFoundItem] = useState<IDynamicForm | null | undefined>(undefined)
9
9
 
10
10
  useEffect(() => {
11
11
  if (!formName) return
12
12
 
13
13
  try {
14
- const storedData = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.DynamicForms)
15
- if (storedData) {
16
- const parsedData: IDynamicForm[] = JSON.parse(storedData)
17
- const item = parsedData.find((entry) => entry.name.split(' ').join('-').toLocaleLowerCase() === formName)
18
-
19
- setFoundItem(item ?? null)
14
+ const stored = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.DynamicForms)
15
+ if (stored) {
16
+ const list: IDynamicForm[] = JSON.parse(stored)
17
+ const match = list.find((entry) => entry.name.split(' ').join('-').toLowerCase() === formName)
18
+ setFoundItem(match ?? null)
19
+ } else {
20
+ setFoundItem(null)
20
21
  }
21
- } catch (error) {
22
- console.error('Error reading or parsing localStorage data', error)
22
+ } catch (err) {
23
+ console.error('Error reading or parsing DynamicForms:', err)
24
+ setFoundItem(null)
23
25
  }
24
26
  }, [formName])
25
27
 
26
- return foundItem?.name?.split(' ').join('-').toLowerCase() === formName ? foundItem : undefined
28
+ const getFormById = useCallback((id: number): IDynamicForm | undefined => {
29
+ if (!id) return
30
+
31
+ try {
32
+ const stored = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.DynamicForms)
33
+ if (!stored) return undefined
34
+
35
+ const list: IDynamicForm[] = JSON.parse(stored)
36
+ return list.find((entry) => entry.id === id)
37
+ } catch (err) {
38
+ console.error('Error in getFormById reading DynamicForms:', err)
39
+ return
40
+ }
41
+ }, [])
42
+
43
+ const formInfo = foundItem && foundItem.name.split(' ').join('-').toLowerCase() === formName ? foundItem : undefined
44
+
45
+ return { formInfo, getFormById }
27
46
  }
@@ -2,7 +2,7 @@ import { useReducer, useState } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
3
  import client, { auth } from '../../../api/client'
4
4
  import { LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
5
- import { cookieHandler } from '../../../global-helpers/cookie-handler'
5
+ import { cookieHandler } from '../../../functions/cookie-handler'
6
6
  import { message } from 'antd'
7
7
  export function useLoginHandler() {
8
8
  const navigate = useNavigate()
@@ -72,20 +72,39 @@ export function useLoginHandler() {
72
72
  setShowVerificationModal(false)
73
73
  }
74
74
  }
75
+
75
76
  const handleForgotPassword = async (email?: string) => {
76
77
  if (!email) {
77
78
  return message.error('Please enter your email or username first.')
78
79
  }
79
80
  const payload = {
80
81
  subject: 'Reset your password',
81
- body: 'Please click the link to reset your password.',
82
+ body: `
83
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px;">
84
+ <h2 style="color: #333;">Reset Your Password</h2>
85
+ <p style="font-size: 16px; color: #555;">
86
+ We received a request to reset your password. Click the button below to verify and continue:
87
+ </p>
88
+ <div style="text-align: center; margin: 30px 0;">
89
+ <a href="${window.location.href.replace('forgot-password', 'reset-password')}?token=[token]&domain=${domain}&email=${email}"
90
+ style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; font-size: 16px; border-radius: 6px;">
91
+ Click here to reset password
92
+ </a>
93
+
94
+ </div>
95
+ <p style="font-size: 14px; color: #888;">
96
+ If you didn’t request this, you can safely ignore this email.
97
+ </p>
98
+ </div>
99
+ `,
82
100
  from: 'no-reply@icodice.com',
83
101
  to: email,
84
102
  cc: '',
85
103
  bcc: '',
86
104
  notificationId: 0,
87
105
  attachments: [],
88
- }
106
+ };
107
+
89
108
 
90
109
  try {
91
110
  await client.post(`/api/user/${domain}/forgotpassword/${email}`, payload)
@@ -95,6 +114,62 @@ export function useLoginHandler() {
95
114
  message.error('Failed to send reset email.')
96
115
  }
97
116
  }
117
+
118
+ const handleVerifyToken = async (domain: string, email: string, token: string) => {
119
+ const encodedEmail = encodeURIComponent(email);
120
+ try {
121
+ const response = await client.post(
122
+ `/api/user/${domain}/verifypasswordtoken/${encodedEmail}`,
123
+ `"${token}"`,
124
+ {
125
+ headers: {
126
+ 'accept': 'text/plain',
127
+ 'Content-Type': 'application/json',
128
+ }
129
+ }
130
+ );
131
+ return response.data;
132
+ } catch (error) {
133
+ message.error('Invalid or expired reset token.');
134
+ return null;
135
+ }
136
+ };
137
+
138
+
139
+ const handleResetPassword = async (
140
+ domain: string,
141
+ email: string,
142
+ token: string,
143
+ password: string
144
+ ) => {
145
+ try {
146
+ const payload = {
147
+ token,
148
+ password,
149
+ notification: {
150
+ subject: 'Password Reset Confirmation',
151
+ body: 'Your password has been successfully reset.',
152
+ from: 'no-reply@icodice.com',
153
+ to: email,
154
+ cc: '',
155
+ bcc: '',
156
+ notificationId: 0,
157
+ attachments: [],
158
+ },
159
+ }
160
+ const encodedEmail = encodeURIComponent(email);
161
+ const response = await client.post(`/api/user/${domain}/resetpassword/${encodedEmail}`, payload)
162
+ if (response.status === 200) {
163
+ message.success('Password successfully reset.')
164
+ navigate('/login')
165
+ }
166
+ return response
167
+ } catch (error) {
168
+ message.error('Failed to reset password.')
169
+ return null
170
+ }
171
+ }
172
+
98
173
  return {
99
174
  handleLogin,
100
175
  loading,
@@ -105,6 +180,8 @@ export function useLoginHandler() {
105
180
  handleSetupCancel,
106
181
  handleVerifyCode,
107
182
  handleUnlinkAuthenticator,
108
- handleForgotPassword
183
+ handleForgotPassword,
184
+ handleVerifyToken,
185
+ handleResetPassword
109
186
  }
110
187
  }
@@ -0,0 +1,30 @@
1
+ import { useEffect, useMemo } from 'react'
2
+ import { useBreadcrumb } from './use-breadcrumb.hook'
3
+ import { FormInstance } from 'antd'
4
+ import { getForeignKeysFromBreadcrumb } from '../../../functions/forms/breadcrumb-handlers'
5
+
6
+ export const useManyToManyConnector = (formRef: FormInstance) => {
7
+ const { breadcrumbs } = useBreadcrumb()
8
+
9
+ const prevPageBreadcrumb = useMemo(() => {
10
+ if (breadcrumbs.length > 2) {
11
+ const prevPageBreadcrumb = breadcrumbs[breadcrumbs.length - 2]
12
+ const { formDataId, formJoins, manyToManyRelInfo } = prevPageBreadcrumb
13
+ if (!formDataId || !formJoins || !Array.isArray(formJoins) || !manyToManyRelInfo) return
14
+
15
+ return prevPageBreadcrumb
16
+ }
17
+
18
+ return
19
+ }, [breadcrumbs])
20
+
21
+ useEffect(() => {
22
+ if (!prevPageBreadcrumb) return
23
+
24
+ const { foreignKey, foreignKey2 } = getForeignKeysFromBreadcrumb(prevPageBreadcrumb)
25
+
26
+ if (foreignKey && foreignKey2) formRef.setFieldValue(foreignKey, prevPageBreadcrumb.formDataId)
27
+ }, [prevPageBreadcrumb, formRef])
28
+
29
+ return
30
+ }
@@ -0,0 +1,21 @@
1
+ import { Result } from 'antd'
2
+ import { Button_FillerPortal } from './button'
3
+ import { useNavigate } from 'react-router-dom'
4
+
5
+ export default function NotFound() {
6
+ const navigate = useNavigate()
7
+ return (
8
+ <Result
9
+ status="404"
10
+ title="404"
11
+ subTitle="Sorry, the page you visited does not exist."
12
+ extra={
13
+ <div className="grid justify-center grid-cols-[200px]">
14
+ <Button_FillerPortal primary onClick={() => navigate('/d')}>
15
+ Go Back
16
+ </Button_FillerPortal>
17
+ </div>
18
+ }
19
+ />
20
+ )
21
+ }
@@ -0,0 +1,42 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export default function SectionPanel({
4
+ header,
5
+ className = '',
6
+ scrollHeight,
7
+ children,
8
+ }: {
9
+ children: ReactNode
10
+ className?: string
11
+ scrollHeight?: string
12
+ header?: {
13
+ title?: any
14
+ description?: any
15
+ action?: any
16
+ }
17
+ }) {
18
+ return (
19
+ <div
20
+ className={`bg-white p-2 rounded-md space-y-2 ${className} ${scrollHeight ? 'fc-scroll' : ''}`}
21
+ style={scrollHeight ? { maxHeight: scrollHeight } : {}}
22
+ >
23
+ {header && (
24
+ <div className="flex items-center justify-between gap-4">
25
+ <div className="flex flex-col leading-5 flex-grow">
26
+ {header.title && <SectionTitle title={header.title} />}
27
+ {header.description && <span className="text-neutral italic text-14">{header.description}</span>}
28
+ </div>
29
+ {header.action && header.action}
30
+ </div>
31
+ )}
32
+
33
+ {children}
34
+ </div>
35
+ )
36
+ }
37
+
38
+ export const SectionTitle = ({ title, size: fontSize = 16 }: { title: any; size?: number }) => (
39
+ <div className="font-bold text-primary" style={{ fontSize }}>
40
+ {title}
41
+ </div>
42
+ )
@@ -0,0 +1,110 @@
1
+ import { Col, Row, Spin, Input, Button, Form, notification } from 'antd'
2
+ import { useState } from 'react'
3
+ import { useConfigContext } from '../context'
4
+ import SectionPanel from '../../common/section-panel'
5
+ import client from '../../../api/client'
6
+ import { UserAuth } from '../../../api/user'
7
+
8
+ export const ChangePassword = () => {
9
+ const { config } = useConfigContext()
10
+ const [loading, setLoading] = useState(false)
11
+ const [form] = Form.useForm()
12
+ const onFinish = async (values: any) => {
13
+ setLoading(true)
14
+ try {
15
+ const response = await client.put('/api/user/changepassword', {
16
+ currentPassword: values.currentPassword,
17
+ newPassword: values.newPassword,
18
+ notification: {
19
+ subject: 'Password Change Notification',
20
+ body: 'Your password has been successfully changed.',
21
+ from: 'no-reply@icodice.com',
22
+ to: UserAuth.getUserEmail(),
23
+ cc: '',
24
+ bcc: '',
25
+ notificationId: 1,
26
+ attachments: [],
27
+ },
28
+ })
29
+ if (response.status === 200) {
30
+ notification.success({
31
+ message: 'Password Changed Successfully',
32
+ description: 'Your password has been updated.',
33
+ })
34
+ }
35
+ } catch (error) {
36
+ notification.error({
37
+ message: 'Password Change Failed',
38
+ description: 'There was an error changing your password.',
39
+ })
40
+ } finally {
41
+ setLoading(false)
42
+ }
43
+ }
44
+
45
+ // Validate that the confirm password matches the new password
46
+ const validateConfirmPassword = (_: any, value: string) => {
47
+ if (!value || value === '') {
48
+ return Promise.resolve()
49
+ }
50
+ if (value !== form.getFieldValue('newPassword')) {
51
+ return Promise.reject(new Error('Confirm password do not match!'))
52
+ }
53
+ return Promise.resolve()
54
+ }
55
+
56
+ return (
57
+ <Spin spinning={!config || loading}>
58
+ <SectionPanel className="mt-2" header={{ title: 'Change Password' }}>
59
+ <Row>
60
+ <Col span={24}>
61
+ <Form
62
+ form={form}
63
+ layout="vertical"
64
+ onFinish={onFinish}
65
+ initialValues={{
66
+ currentPassword: '',
67
+ newPassword: '',
68
+ confirmPassword: '',
69
+ }}
70
+ >
71
+ <Form.Item
72
+ label="Current Password"
73
+ name="currentPassword"
74
+ rules={[{ required: true, message: 'Please enter your current password' }]}
75
+ >
76
+ <Input.Password placeholder="Enter current password" />
77
+ </Form.Item>
78
+
79
+ <Form.Item
80
+ label="New Password"
81
+ name="newPassword"
82
+ rules={[{ required: true, message: 'Please enter your new password' }]}
83
+ >
84
+ <Input.Password placeholder="Enter new password" />
85
+ </Form.Item>
86
+
87
+ <Form.Item
88
+ label="Confirm New Password"
89
+ name="confirmPassword"
90
+ rules={[
91
+ { required: true, message: 'Please confirm your new password' },
92
+ { validator: validateConfirmPassword },
93
+ ]}
94
+ >
95
+ <Input.Password placeholder="Confirm new password" />
96
+ </Form.Item>
97
+
98
+ {/* Add margin to space between fields and button */}
99
+ <Form.Item style={{ marginTop: '16px' }}>
100
+ <Button type="primary" htmlType="submit" loading={loading}>
101
+ Change Password
102
+ </Button>
103
+ </Form.Item>
104
+ </Form>
105
+ </Col>
106
+ </Row>
107
+ </SectionPanel>
108
+ </Spin>
109
+ )
110
+ }