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.
- package/package.json +3 -2
- package/src/api/client.ts +1 -1
- package/src/api/user.ts +7 -8
- package/src/components/common/custom-hooks/use-breadcrumb.hook.ts +90 -0
- package/src/components/common/custom-hooks/use-check-element-conditions.hook.ts +3 -3
- package/src/components/common/custom-hooks/use-find-dynamic-form.hook.ts +30 -11
- package/src/components/common/custom-hooks/use-login-handler.ts +81 -4
- package/src/components/common/custom-hooks/use-many-to-many-connector.hook.ts +30 -0
- package/src/components/common/not-found.tsx +21 -0
- package/src/components/common/section-panel.tsx +42 -0
- package/src/components/companies/1-authenticated/change-password.tsx +110 -0
- package/src/components/companies/2-unauthenticated/reset-password.tsx +128 -0
- package/src/components/companies/index.tsx +2 -0
- package/src/components/form/1-list/index.tsx +37 -38
- package/src/components/form/1-list/table-header.tsx +6 -6
- package/src/components/form/1-list/table.tsx +10 -12
- package/src/components/form/2-details/index.tsx +63 -41
- package/src/components/form/layout-renderer/1-row/index.tsx +12 -5
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +59 -89
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-navigate.hook.tsx +88 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-create-data.hook.ts +22 -23
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +3 -4
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-save-data.hook.ts +2 -2
- package/src/components/form/layout-renderer/3-element/11-breadcrumb.tsx +30 -0
- package/src/components/form/layout-renderer/3-element/2-field-element.tsx +4 -1
- package/src/components/form/layout-renderer/3-element/4-rich-text-editor.tsx +27 -2
- package/src/components/form/layout-renderer/3-element/6-signature.tsx +5 -1
- package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +115 -74
- package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +50 -110
- package/src/components/form/layout-renderer/3-element/index.tsx +27 -9
- package/src/components/modals/report-filters.modal/helper-functions.ts +3 -6
- package/src/constants.ts +62 -45
- package/src/enums/form.enum.ts +5 -3
- package/src/enums/index.ts +9 -3
- package/src/{global-helpers → functions}/cookie-handler.ts +9 -10
- package/src/functions/forms/breadcrumb-handlers.ts +21 -0
- package/src/functions/forms/create-form-rules.ts +4 -1
- package/src/functions/forms/data-render-functions.tsx +5 -4
- package/src/functions/forms/extended-json-handlers.ts +56 -0
- package/src/functions/forms/index.ts +17 -11
- package/src/functions/index.ts +2 -1
- package/src/functions/reports/index.tsx +2 -1
- package/src/types/companies/site-layout/authenticated/index.tsx +23 -14
- package/src/types/forms/data-list/index.ts +0 -7
- package/src/types/forms/index.ts +1 -0
- package/src/types/forms/layout-elements/button.ts +11 -3
- package/src/types/forms/layout-elements/data-render-config.ts +1 -0
- package/src/types/forms/layout-elements/index.ts +12 -2
- package/src/types/forms/layout-elements/sanitization.ts +6 -1
- package/src/types/forms/relationship/index.ts +12 -1
- package/src/types/index.ts +2 -0
- package/src/functions/forms/json-handlers.ts +0 -19
- package/src/global-helpers/constants.ts +0 -2
- 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.
|
|
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.
|
|
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 '../
|
|
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
|
|
2
|
-
import {
|
|
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 {
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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 |
|
|
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
|
|
15
|
-
if (
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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 (
|
|
22
|
-
console.error('Error reading or parsing
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error('Error reading or parsing DynamicForms:', err)
|
|
24
|
+
setFoundItem(null)
|
|
23
25
|
}
|
|
24
26
|
}, [formName])
|
|
25
27
|
|
|
26
|
-
|
|
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 '../../../
|
|
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:
|
|
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
|
+
}
|