form-craft-package 1.7.10-dev.2 → 1.7.10-dev.3
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 +1 -1
- package/src/api/client.ts +6 -2
- package/src/components/common/custom-hooks/use-breadcrumb.hook.ts +7 -4
- package/src/components/common/custom-hooks/use-dayjs-extender.hook.ts +2 -0
- package/src/components/common/custom-hooks/use-login-handler.ts +143 -147
- package/src/components/companies/2-unauthenticated/index.tsx +0 -1
- package/src/components/form/1-list/index.tsx +2 -8
- package/src/components/form/1-list/table.tsx +15 -2
- package/src/components/form/2-details/index.tsx +17 -5
- package/src/components/form/layout-renderer/1-row/index.tsx +2 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +4 -4
- package/src/components/form/layout-renderer/3-element/11-breadcrumb.tsx +12 -2
- package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +131 -47
- package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +13 -9
- package/src/components/form/layout-renderer/3-element/index.tsx +2 -1
- package/src/components/modals/report-filters.modal/index.tsx +1 -1
- package/src/components/report/1-list/index.tsx +1 -1
- package/src/enums/form.enum.ts +5 -0
- package/src/functions/forms/data-render-functions.tsx +4 -0
- package/src/functions/forms/extended-json-handlers.ts +4 -5
- package/src/functions/forms/get-element-props.ts +13 -2
- package/src/types/companies/site-layout/authenticated/index.tsx +3 -2
- package/src/types/forms/data-list/index.ts +1 -0
- package/src/types/forms/index.ts +1 -1
- package/src/types/forms/layout-elements/data-render-config.ts +1 -0
- package/src/types/forms/layout-elements/index.ts +41 -28
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Cookies from 'js-cookie'
|
|
|
2
2
|
import axios, { AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios'
|
|
3
3
|
import qs from 'qs'
|
|
4
4
|
import { notification } from 'antd'
|
|
5
|
-
import { LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS } from '../enums'
|
|
5
|
+
import { BreadcrumbTypeEnum, LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS } from '../enums'
|
|
6
6
|
|
|
7
7
|
import { IDynamicForm } from '../types'
|
|
8
8
|
import { constructDynamicFormHref, fetchDynamicForms } from '../functions'
|
|
@@ -77,7 +77,11 @@ const auth = async (
|
|
|
77
77
|
const firstForm = forms[0]
|
|
78
78
|
const splittedFormName = firstForm.name.split(' ')?.[0] ?? ''
|
|
79
79
|
|
|
80
|
-
breadcrumbStore.push({
|
|
80
|
+
breadcrumbStore.push({
|
|
81
|
+
label: splittedFormName,
|
|
82
|
+
href: constructDynamicFormHref(firstForm.name),
|
|
83
|
+
type: BreadcrumbTypeEnum.List,
|
|
84
|
+
})
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
localStorage.setItem(LOCAL_STORAGE_KEYS_ENUM.DynamicForms, JSON.stringify(forms))
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react'
|
|
2
|
-
import { LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
|
|
2
|
+
import { BreadcrumbTypeEnum, LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
|
|
3
3
|
import { NEW_FORM_DATA_IDENTIFIER } from '../../../constants'
|
|
4
4
|
|
|
5
|
-
export type IBreadcrumb = { label: string; href: string; [key: string]: any }
|
|
5
|
+
export type IBreadcrumb = { label: string; href: string; [key: string]: any; type: BreadcrumbTypeEnum }
|
|
6
6
|
|
|
7
7
|
let crumbs: IBreadcrumb[] = loadFromStorage()
|
|
8
8
|
const listeners = new Set<() => void>()
|
|
@@ -36,8 +36,11 @@ function dedupeAdjacentCrumbs(crumbs: IBreadcrumb[]): IBreadcrumb[] {
|
|
|
36
36
|
|
|
37
37
|
const result: IBreadcrumb[] = [crumbs[0]]
|
|
38
38
|
for (let i = 1; i < crumbs.length; i++) {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const curr = crumbs[i]
|
|
40
|
+
const prev = crumbs[i - 1]
|
|
41
|
+
if (curr.href !== prev.href) {
|
|
42
|
+
if (curr.label === prev.label && curr.type === prev.type) result.splice(i - 1, 1)
|
|
43
|
+
result.push(curr)
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
return result
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import dayjs from 'dayjs'
|
|
2
2
|
import localeData from 'dayjs/plugin/localeData'
|
|
3
3
|
import weekday from 'dayjs/plugin/weekday'
|
|
4
|
+
import utc from 'dayjs/plugin/utc'
|
|
4
5
|
|
|
5
6
|
dayjs.extend(localeData)
|
|
6
7
|
dayjs.extend(weekday)
|
|
8
|
+
dayjs.extend(utc)
|
|
7
9
|
|
|
8
10
|
export const useDayjsExtender = () => {}
|
|
@@ -5,88 +5,95 @@ import { LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
|
|
|
5
5
|
import { cookieHandler } from '../../../functions/cookie-handler'
|
|
6
6
|
import { message } from 'antd'
|
|
7
7
|
export function useLoginHandler() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
8
|
+
const navigate = useNavigate()
|
|
9
|
+
const domain = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.Domain) || ''
|
|
10
|
+
const [loading, updateLoading] = useReducer((currState: any, updated: any) => ({ ...currState, ...updated }), {
|
|
11
|
+
login: false,
|
|
12
|
+
})
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const [googleAuthSetup, setGoogleAuthSetup] = useState<null | {
|
|
15
|
+
qrCodeImageUrl: string
|
|
16
|
+
manualEntryCode: string
|
|
17
|
+
key: string
|
|
18
|
+
}>(null)
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const [showVerificationModal, setShowVerificationModal] = useState(false)
|
|
21
|
+
const [latestLoginValues, setLatestLoginValues] = useState<any>(null)
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const handleLogin = async (values: any) => {
|
|
24
|
+
updateLoading({ login: true })
|
|
25
|
+
setLatestLoginValues(values)
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
const { authRes, initialPath } = await auth(
|
|
28
|
+
values.email,
|
|
29
|
+
values.password,
|
|
30
|
+
localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.Domain) ?? '',
|
|
31
|
+
values.type || 'password',
|
|
32
|
+
)
|
|
33
|
+
if (authRes.status === 200) {
|
|
34
|
+
const data = authRes.data
|
|
35
|
+
if (!data.required2FA) {
|
|
36
|
+
navigate(initialPath)
|
|
37
|
+
} else if (data.required2FA && !data.configuredGoogleAuth) {
|
|
38
|
+
const googleSetup = await client.get(`api/authorization/authkey`)
|
|
39
|
+
if (googleSetup.status === 200) {
|
|
40
|
+
setGoogleAuthSetup({
|
|
41
|
+
qrCodeImageUrl: googleSetup.data.qrCodeImageUrl,
|
|
42
|
+
manualEntryCode: googleSetup.data.manualEntrySetupCode,
|
|
43
|
+
key: googleSetup.data.key,
|
|
44
|
+
})
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
} else if (data.required2FA && data.configuredGoogleAuth) {
|
|
47
|
+
setShowVerificationModal(true)
|
|
48
|
+
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await client.put(`api/authorization/authkey?key=${googleAuthSetup.key}`)
|
|
53
|
-
setGoogleAuthSetup(null)
|
|
54
|
-
handleLogin(latestLoginValues)
|
|
55
|
-
}
|
|
56
|
-
const handleSetupCancel = async () => {
|
|
57
|
-
await cookieHandler.empty()
|
|
58
|
-
setGoogleAuthSetup(null)
|
|
59
|
-
}
|
|
51
|
+
updateLoading({ login: false })
|
|
52
|
+
}
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
54
|
+
const handleSetupDone = async () => {
|
|
55
|
+
if (!googleAuthSetup) return
|
|
56
|
+
await client.put(`api/authorization/authkey?key=${googleAuthSetup.key}`)
|
|
57
|
+
setGoogleAuthSetup(null)
|
|
58
|
+
handleLogin(latestLoginValues)
|
|
59
|
+
}
|
|
60
|
+
const handleSetupCancel = async () => {
|
|
61
|
+
await cookieHandler.empty()
|
|
62
|
+
setGoogleAuthSetup(null)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleVerifyCode = async (code: any) => {
|
|
66
|
+
await handleLogin({ email: latestLoginValues.email, password: code, type: 'refresh_token' })
|
|
67
|
+
}
|
|
68
|
+
const handleUnlinkAuthenticator = async () => {
|
|
69
|
+
const res = await client.get('api/authorization/authkey')
|
|
70
|
+
if (res.status === 200) {
|
|
71
|
+
setGoogleAuthSetup({
|
|
72
|
+
qrCodeImageUrl: res.data.qrCodeImageUrl,
|
|
73
|
+
manualEntryCode: res.data.manualEntrySetupCode,
|
|
74
|
+
key: res.data.key,
|
|
75
|
+
})
|
|
76
|
+
setShowVerificationModal(false)
|
|
74
77
|
}
|
|
78
|
+
}
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
const handleForgotPassword = async (email?: string) => {
|
|
81
|
+
if (!email) {
|
|
82
|
+
return message.error('Please enter your email or username first.')
|
|
83
|
+
}
|
|
84
|
+
const payload = {
|
|
85
|
+
subject: 'Reset your password',
|
|
86
|
+
body: `
|
|
83
87
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px;">
|
|
84
88
|
<h2 style="color: #333;">Reset Your Password</h2>
|
|
85
89
|
<p style="font-size: 16px; color: #555;">
|
|
86
90
|
We received a request to reset your password. Click the button below to verify and continue:
|
|
87
91
|
</p>
|
|
88
92
|
<div style="text-align: center; margin: 30px 0;">
|
|
89
|
-
<a href="${window.location.href.replace(
|
|
93
|
+
<a href="${window.location.href.replace(
|
|
94
|
+
'forgot-password',
|
|
95
|
+
'reset-password',
|
|
96
|
+
)}?token=[token]&domain=${domain}&email=${email}"
|
|
90
97
|
style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; font-size: 16px; border-radius: 6px;">
|
|
91
98
|
Click here to reset password
|
|
92
99
|
</a>
|
|
@@ -97,91 +104,80 @@ export function useLoginHandler() {
|
|
|
97
104
|
</p>
|
|
98
105
|
</div>
|
|
99
106
|
`,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
await client.post(`/api/user/${domain}/forgotpassword/${email}`, payload)
|
|
111
|
-
message.success('Reset link sent. Please check your email.')
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.error('Forgot Password Error:', error)
|
|
114
|
-
message.error('Failed to send reset email.')
|
|
115
|
-
}
|
|
107
|
+
from: 'no-reply@icodice.com',
|
|
108
|
+
to: email,
|
|
109
|
+
cc: '',
|
|
110
|
+
bcc: '',
|
|
111
|
+
notificationId: 0,
|
|
112
|
+
attachments: [],
|
|
116
113
|
}
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
115
|
+
try {
|
|
116
|
+
await client.post(`/api/user/${domain}/forgotpassword/${email}`, payload)
|
|
117
|
+
message.success('Reset link sent. Please check your email.')
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Forgot Password Error:', error)
|
|
120
|
+
message.error('Failed to send reset email.')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
}
|
|
124
|
+
const handleVerifyToken = async (domain: string, email: string, token: string) => {
|
|
125
|
+
const encodedEmail = encodeURIComponent(email)
|
|
126
|
+
try {
|
|
127
|
+
const response = await client.post(`/api/user/${domain}/verifypasswordtoken/${encodedEmail}`, `"${token}"`, {
|
|
128
|
+
headers: {
|
|
129
|
+
accept: 'text/plain',
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
return response.data
|
|
134
|
+
} catch (error) {
|
|
135
|
+
message.error('Invalid or expired reset token.')
|
|
136
|
+
return null
|
|
171
137
|
}
|
|
138
|
+
}
|
|
172
139
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
140
|
+
const handleResetPassword = async (domain: string, email: string, token: string, password: string) => {
|
|
141
|
+
try {
|
|
142
|
+
const payload = {
|
|
143
|
+
token,
|
|
144
|
+
password,
|
|
145
|
+
notification: {
|
|
146
|
+
subject: 'Password Reset Confirmation',
|
|
147
|
+
body: 'Your password has been successfully reset.',
|
|
148
|
+
from: 'no-reply@icodice.com',
|
|
149
|
+
to: email,
|
|
150
|
+
cc: '',
|
|
151
|
+
bcc: '',
|
|
152
|
+
notificationId: 0,
|
|
153
|
+
attachments: [],
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
const encodedEmail = encodeURIComponent(email)
|
|
157
|
+
const response = await client.post(`/api/user/${domain}/resetpassword/${encodedEmail}`, payload)
|
|
158
|
+
if (response.status === 200) {
|
|
159
|
+
message.success('Password successfully reset.')
|
|
160
|
+
navigate('/login')
|
|
161
|
+
}
|
|
162
|
+
return response
|
|
163
|
+
} catch (error) {
|
|
164
|
+
message.error('Failed to reset password.')
|
|
165
|
+
return null
|
|
186
166
|
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
handleLogin,
|
|
171
|
+
loading,
|
|
172
|
+
googleAuthSetup,
|
|
173
|
+
showVerificationModal,
|
|
174
|
+
handleSetupDone,
|
|
175
|
+
setShowVerificationModal,
|
|
176
|
+
handleSetupCancel,
|
|
177
|
+
handleVerifyCode,
|
|
178
|
+
handleUnlinkAuthenticator,
|
|
179
|
+
handleForgotPassword,
|
|
180
|
+
handleVerifyToken,
|
|
181
|
+
handleResetPassword,
|
|
182
|
+
}
|
|
187
183
|
}
|
|
@@ -6,7 +6,6 @@ import { useNavigate } from 'react-router-dom'
|
|
|
6
6
|
|
|
7
7
|
export function UnauthenticatedLayout() {
|
|
8
8
|
const { config } = useConfigContext()
|
|
9
|
-
console.log(config?.siteLanguages)
|
|
10
9
|
const navigate = useNavigate()
|
|
11
10
|
const loginLayout = config?.loginLayout
|
|
12
11
|
const siteIdentity = config?.siteIdentity
|
|
@@ -37,13 +37,12 @@ function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunc
|
|
|
37
37
|
const dataListConfig: IFormDataListConfig = res.data.Data.dataListConfig
|
|
38
38
|
const { columns, pagination, defaultFilter, defaultSorter, header } = dataListConfig
|
|
39
39
|
|
|
40
|
-
if (Array.isArray(columns))
|
|
40
|
+
if (Array.isArray(columns)) {
|
|
41
41
|
dataProjectRef.current = columns.reduce(
|
|
42
42
|
(curr, c) => (c.key ? { ...curr, [getProjectionKey(c.key)]: `$${c.key}` } : curr),
|
|
43
43
|
{},
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
if (Array.isArray(columns))
|
|
47
46
|
formJoinsRef.current = mergeJoins([
|
|
48
47
|
...columns.map((c) => c.joins),
|
|
49
48
|
...Object.values(header.elements).reduce((c: IFormJoin[], el) => {
|
|
@@ -53,7 +52,7 @@ function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunc
|
|
|
53
52
|
return c
|
|
54
53
|
}, []),
|
|
55
54
|
])
|
|
56
|
-
|
|
55
|
+
}
|
|
57
56
|
const defaultReqData: IDataListReqData = {}
|
|
58
57
|
if (!pagination?.hasNoPagination) {
|
|
59
58
|
defaultReqData.current = 1
|
|
@@ -100,11 +99,6 @@ function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunc
|
|
|
100
99
|
|
|
101
100
|
const { request, cancel } = cancelableClient.post(`/api/report/data/${formId}`, {
|
|
102
101
|
joins: formJoinsRef.current,
|
|
103
|
-
// group: JSON.stringify({
|
|
104
|
-
// _id: '$_id',
|
|
105
|
-
// ...Object.entries(dataProjectRef.current).reduce((c, n) => ({ ...c, [n[0]]: { $first: n[1] } }), {}),
|
|
106
|
-
// }),
|
|
107
|
-
// project: JSON.stringify(Object.keys(dataProjectRef.current).reduce((c, n) => ({ ...c, [n]: 1 }), {})),
|
|
108
102
|
project: JSON.stringify(dataProjectRef.current),
|
|
109
103
|
...restReqData,
|
|
110
104
|
})
|
|
@@ -4,14 +4,15 @@ import useDebounced from '../../common/custom-hooks/use-debounce.hook'
|
|
|
4
4
|
import { Table } from 'antd'
|
|
5
5
|
import { ICustomFunctionCall } from '../layout-renderer/3-element/1-dynamic-button'
|
|
6
6
|
import { SorterResult } from 'antd/es/table/interface'
|
|
7
|
-
import { renderTableColumns, revertProjectionKey } from '../../../functions/forms'
|
|
8
|
-
import { useLocation } from 'react-router-dom'
|
|
7
|
+
import { constructDynamicFormHref, renderTableColumns, revertProjectionKey } from '../../../functions/forms'
|
|
8
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
9
9
|
import useGetCurrentBreakpoint from '../../common/custom-hooks/use-window-width.hook'
|
|
10
10
|
import { DeviceBreakpointEnum, FormDataListViewTypeEnum, MongoDbSortOrderEnum } from '../../../enums'
|
|
11
11
|
import { IDataListReqData } from '.'
|
|
12
12
|
import { IFormDataListConfig, IFormDataListData, IFormDataTableColumn } from '../../../types'
|
|
13
13
|
import { processOptions } from '../../../functions/forms/get-data-list-option-value'
|
|
14
14
|
import { IFormContext } from '../layout-renderer/1-row'
|
|
15
|
+
import { useFindDynamiForm } from '../../common/custom-hooks'
|
|
15
16
|
|
|
16
17
|
export default function FormDataListTableComponent({
|
|
17
18
|
layoutsConfigs,
|
|
@@ -22,7 +23,9 @@ export default function FormDataListTableComponent({
|
|
|
22
23
|
setParentLoading,
|
|
23
24
|
headerLayoutContext,
|
|
24
25
|
}: IFormDataListTableComponent) {
|
|
26
|
+
const { getFormById } = useFindDynamiForm()
|
|
25
27
|
const currentBreakpoint = useGetCurrentBreakpoint()
|
|
28
|
+
const navigate = useNavigate()
|
|
26
29
|
const location = useLocation()
|
|
27
30
|
const [loading, setLoading] = useState(true)
|
|
28
31
|
const [tableColumns, setTableColumns] = useState<(IFormDataTableColumn & { hideScreens?: DeviceBreakpointEnum[] })[]>(
|
|
@@ -122,6 +125,16 @@ export default function FormDataListTableComponent({
|
|
|
122
125
|
dataSource={dataList.data}
|
|
123
126
|
columns={tableColumnsFiltered}
|
|
124
127
|
rowKey={(record) => record._id}
|
|
128
|
+
rowClassName={() => (layoutsConfigs?.dataListConfig?.onRowClickToNavigate ? 'cursor-pointer' : '')}
|
|
129
|
+
onRow={(record) => ({
|
|
130
|
+
onClick: () => {
|
|
131
|
+
if (layoutsConfigs?.dataListConfig?.onRowClickToNavigate && headerLayoutContext?.formId) {
|
|
132
|
+
const formInfo = getFormById(headerLayoutContext.formId)
|
|
133
|
+
if (!formInfo) return
|
|
134
|
+
navigate(`${constructDynamicFormHref(formInfo.name)}/${record._id}`)
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
})}
|
|
125
138
|
pagination={
|
|
126
139
|
otherConfigs?.pagination?.hasNoPagination
|
|
127
140
|
? false
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { Form } from 'antd'
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
3
|
import { IDndLayoutStructure_Responsive, IFormSchema } from '../../../types'
|
|
4
4
|
import { LayoutRendererRow } from '../layout-renderer/1-row'
|
|
5
5
|
import { DynamicFormButtonRender, ICustomFunctionCall } from '../layout-renderer/3-element/1-dynamic-button'
|
|
6
6
|
import FormDataListSkeleton_Details from '../../common/loading-skeletons/details'
|
|
7
7
|
import { useLocation } from 'react-router-dom'
|
|
8
|
-
import { DeviceBreakpointEnum, FormPreservedItemKeys, LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
|
|
9
8
|
import useGetCurrentBreakpoint from '../../common/custom-hooks/use-window-width.hook'
|
|
10
9
|
import client from '../../../api/client'
|
|
11
10
|
import { useBreadcrumb } from '../../common/custom-hooks/use-breadcrumb.hook'
|
|
12
11
|
import NotFound from '../../common/not-found'
|
|
13
12
|
import { useManyToManyConnector } from '../../common/custom-hooks/use-many-to-many-connector.hook'
|
|
13
|
+
import {
|
|
14
|
+
BreadcrumbTypeEnum,
|
|
15
|
+
DeviceBreakpointEnum,
|
|
16
|
+
FormPreservedItemKeys,
|
|
17
|
+
LOCAL_STORAGE_KEYS_ENUM,
|
|
18
|
+
} from '../../../enums'
|
|
14
19
|
import {
|
|
15
20
|
fromMongoDbExtendedJSON,
|
|
21
|
+
getPickerFieldsWithOriginalTz,
|
|
16
22
|
isNewFormDataPage,
|
|
17
23
|
isValidMongoDbId,
|
|
18
24
|
queryParamsToObject,
|
|
@@ -35,6 +41,7 @@ export default function FormDataDetailsComponent({
|
|
|
35
41
|
const [loadings, setLoadings] = useState({ layout: true, data: true })
|
|
36
42
|
const [layoutConfig, setLayoutConfig] = useState<IDndLayoutStructure_Responsive>({ elements: {}, layouts: {} })
|
|
37
43
|
const [isNotFound, setIsNotFound] = useState(false)
|
|
44
|
+
const originalTzFieldsRef = useRef<string[]>([])
|
|
38
45
|
useManyToManyConnector(formDataRef)
|
|
39
46
|
|
|
40
47
|
const currentBreakpoint = useGetCurrentBreakpoint()
|
|
@@ -52,8 +59,9 @@ export default function FormDataDetailsComponent({
|
|
|
52
59
|
useEffect(() => {
|
|
53
60
|
const splittedFormName = formName?.split(' ')?.[0] ?? ''
|
|
54
61
|
push({
|
|
55
|
-
label: isNewFormDataPage(formDataId) ?
|
|
62
|
+
label: isNewFormDataPage(formDataId) ? splittedFormName.toLowerCase() : splittedFormName,
|
|
56
63
|
href: location.pathname,
|
|
64
|
+
type: isNewFormDataPage(formDataId) ? BreadcrumbTypeEnum.New : BreadcrumbTypeEnum.Details,
|
|
57
65
|
})
|
|
58
66
|
}, [location.pathname, formName, formDataId])
|
|
59
67
|
|
|
@@ -101,7 +109,10 @@ export default function FormDataDetailsComponent({
|
|
|
101
109
|
const { data: jsonData, ...restFormData } = res.data
|
|
102
110
|
let parsedFormData = JSON.parse(jsonData)
|
|
103
111
|
if (parsedFormData) {
|
|
104
|
-
const fieldsValue: { [key: string]: any } = fromMongoDbExtendedJSON(
|
|
112
|
+
const fieldsValue: { [key: string]: any } = fromMongoDbExtendedJSON(
|
|
113
|
+
parsedFormData,
|
|
114
|
+
originalTzFieldsRef.current,
|
|
115
|
+
)
|
|
105
116
|
formDataRef.setFieldsValue({ ...restFormData, ...fieldsValue })
|
|
106
117
|
|
|
107
118
|
console.log('FORM DATA FETCH (PARSED)', fieldsValue)
|
|
@@ -125,6 +136,7 @@ export default function FormDataDetailsComponent({
|
|
|
125
136
|
if (parsedData) {
|
|
126
137
|
console.log('FORM LAYOUT FETCH (PARSED)', parsedData)
|
|
127
138
|
const { layouts: rawLayouts, elements } = parsedData.detailsConfig
|
|
139
|
+
originalTzFieldsRef.current = getPickerFieldsWithOriginalTz(elements)
|
|
128
140
|
|
|
129
141
|
if (isPublic) {
|
|
130
142
|
if (parsedData.generateConfig?.submissionPdf?.enabled)
|
|
@@ -144,7 +156,7 @@ export default function FormDataDetailsComponent({
|
|
|
144
156
|
}, [formId, formKey, isPublic, formDataRef, fetchFormData])
|
|
145
157
|
|
|
146
158
|
const formContext = useMemo(
|
|
147
|
-
() => ({ formId, formKey, formDataId, formRef: formDataRef, companyKey }),
|
|
159
|
+
() => ({ detailPageFormId: formId, formId, formKey, formDataId, formRef: formDataRef, companyKey }),
|
|
148
160
|
[formDataId, formDataRef, formId, formKey, companyKey],
|
|
149
161
|
)
|
|
150
162
|
|
|
@@ -226,7 +226,7 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
226
226
|
loading={loading}
|
|
227
227
|
disabled={isElementDisabled}
|
|
228
228
|
className="w-full"
|
|
229
|
-
onClick={() => {
|
|
229
|
+
onClick={(e) => {
|
|
230
230
|
if (inPreviewMode) return
|
|
231
231
|
if (
|
|
232
232
|
isPublic &&
|
|
@@ -234,7 +234,9 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
234
234
|
![ButtonActionCategoryEnum.SaveDataChanges, ButtonActionCategoryEnum.Navigate].includes(btnProps.category)
|
|
235
235
|
)
|
|
236
236
|
return
|
|
237
|
+
e.stopPropagation()
|
|
237
238
|
setLoading(true)
|
|
239
|
+
|
|
238
240
|
if (btnProps.confirmation?.enabled) {
|
|
239
241
|
confirmModal({
|
|
240
242
|
content: btnProps.confirmation.message,
|
|
@@ -242,9 +244,7 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
242
244
|
cancelText: btnProps.confirmation.cancelLabel,
|
|
243
245
|
onOk: handleButtonClick,
|
|
244
246
|
})
|
|
245
|
-
} else
|
|
246
|
-
handleButtonClick()
|
|
247
|
-
}
|
|
247
|
+
} else handleButtonClick()
|
|
248
248
|
}}
|
|
249
249
|
>
|
|
250
250
|
{isPublic &&
|
|
@@ -3,8 +3,10 @@ import { Button_FillerPortal } from '../../../common/button'
|
|
|
3
3
|
import { useNavigate } from 'react-router-dom'
|
|
4
4
|
import { FaChevronRight } from 'react-icons/fa6'
|
|
5
5
|
import React from 'react'
|
|
6
|
+
import { BreadcrumbTypeEnum } from '../../../../enums'
|
|
7
|
+
import { IBreadcrumbElementProps } from '../../../../types'
|
|
6
8
|
|
|
7
|
-
export default function LayoutRenderer_Breadcrumb() {
|
|
9
|
+
export default function LayoutRenderer_Breadcrumb({ elementProps }: { elementProps: IBreadcrumbElementProps }) {
|
|
8
10
|
const navigate = useNavigate()
|
|
9
11
|
const { breadcrumbs, sliceAt } = useBreadcrumb()
|
|
10
12
|
|
|
@@ -20,7 +22,15 @@ export default function LayoutRenderer_Breadcrumb() {
|
|
|
20
22
|
navigate(bc.href)
|
|
21
23
|
}}
|
|
22
24
|
>
|
|
23
|
-
<span className="font-normal italic">
|
|
25
|
+
<span className="font-normal italic">
|
|
26
|
+
{bc.type === BreadcrumbTypeEnum.New ? `${elementProps.newText} ` : ''}
|
|
27
|
+
{bc.label}
|
|
28
|
+
{bc.type === BreadcrumbTypeEnum.Details
|
|
29
|
+
? ` ${elementProps.detailText}`
|
|
30
|
+
: bc.type === BreadcrumbTypeEnum.List
|
|
31
|
+
? ` ${elementProps.listText}`
|
|
32
|
+
: ''}
|
|
33
|
+
</span>
|
|
24
34
|
</Button_FillerPortal>
|
|
25
35
|
{breadcrumbs.length - 1 !== bcIdx && <FaChevronRight className="text-primary" />}
|
|
26
36
|
</React.Fragment>
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { FieldElementOptionSourceEnum } from '../../../../enums'
|
|
1
|
+
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { BreadcrumbTypeEnum, FieldElementOptionSourceEnum, FilterConfigTypeEnum } from '../../../../enums'
|
|
3
3
|
import { IElementBaseProps } from '.'
|
|
4
4
|
import { LayoutRenderer_FieldElement } from './2-field-element'
|
|
5
5
|
import { getElementGeneralizedProps } from '../../../../functions/forms/get-element-props'
|
|
6
6
|
import { useFormPreservedItemValues } from '../../../common/custom-hooks/use-preserved-form-items.hook'
|
|
7
7
|
import { IFormContext } from '../1-row'
|
|
8
|
-
import
|
|
9
|
-
import { CancelToken } from 'axios'
|
|
8
|
+
import { cancelableClient } from '../../../../api/client'
|
|
10
9
|
import { useBreadcrumb } from '../../../common/custom-hooks/use-breadcrumb.hook'
|
|
11
10
|
import { Form, Spin } from 'antd'
|
|
11
|
+
import { useFindDynamiForm } from '../../../common/custom-hooks'
|
|
12
|
+
import { useNavigate } from 'react-router-dom'
|
|
12
13
|
import { constructDynamicFormHref, getIdEqualsQuery, isNewFormDataPage } from '../../../../functions'
|
|
13
14
|
import {
|
|
15
|
+
IFilterConfig,
|
|
14
16
|
IFormJoin,
|
|
15
17
|
IFormLayoutFieldOption,
|
|
18
|
+
IOptionDependentOn,
|
|
16
19
|
IOptionSourceConstant,
|
|
17
20
|
IOptionSourceDynamicForm,
|
|
18
21
|
IOptionSourceLinkedForm,
|
|
@@ -20,8 +23,6 @@ import {
|
|
|
20
23
|
IRadioElement,
|
|
21
24
|
ISelectElement,
|
|
22
25
|
} from '../../../../types'
|
|
23
|
-
import { useFindDynamiForm } from '../../../common/custom-hooks'
|
|
24
|
-
import { useNavigate } from 'react-router-dom'
|
|
25
26
|
|
|
26
27
|
export default function LayoutRenderer_FieldsWithOptions({
|
|
27
28
|
formContext,
|
|
@@ -31,17 +32,43 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
31
32
|
}: ILayoutRenderer_FieldsWithOptions) {
|
|
32
33
|
const navigate = useNavigate()
|
|
33
34
|
const { getFormById } = useFindDynamiForm()
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const cancelTokenRef = useRef<CancelToken | undefined>(undefined)
|
|
35
|
+
|
|
36
|
+
const { formRef, formId, formDataId, detailPageFormId } = formContext
|
|
37
|
+
const { inPreviewMode } = useFormPreservedItemValues(formRef)
|
|
38
38
|
const { breadcrumbs, push } = useBreadcrumb()
|
|
39
|
+
|
|
40
|
+
const props = getElementGeneralizedProps(elementData.props)
|
|
41
|
+
|
|
42
|
+
const [options, setOptions] = useState<{ value: string; label: ReactNode }[]>([])
|
|
39
43
|
const [loading, setLoading] = useState(true)
|
|
44
|
+
|
|
40
45
|
const isManyToManyPageRef = useRef(false)
|
|
46
|
+
const dependentOnRef = useRef<IOptionDependentOn | undefined>(props.dependentOn)
|
|
47
|
+
const filterRef = useRef<IFilterConfig | undefined>(props.filter)
|
|
48
|
+
const dependentFieldPrevValueRef = useRef<any>(null)
|
|
49
|
+
const selectedValuePrefRef = useRef<string | null>(null)
|
|
50
|
+
|
|
51
|
+
const cancelReqFuncRef = useRef<{
|
|
52
|
+
otherForm?: () => void
|
|
53
|
+
linkedForm?: () => void
|
|
54
|
+
linkedFormTemp?: () => void
|
|
55
|
+
staticOptions?: () => void
|
|
56
|
+
}>({})
|
|
41
57
|
|
|
42
58
|
const selectedValue = Form.useWatch(formItem.path, formRef)
|
|
59
|
+
const dependentFieldValue = dependentOnRef.current ? Form.useWatch(dependentOnRef.current.field, formRef) : undefined
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (dependentOnRef.current && dependentFieldValue !== dependentFieldPrevValueRef.current) {
|
|
63
|
+
dependentFieldPrevValueRef.current = dependentFieldValue
|
|
64
|
+
if (!dependentFieldValue) setOptions([])
|
|
65
|
+
setLoading(true)
|
|
66
|
+
}
|
|
67
|
+
}, [dependentFieldValue])
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!selectedValuePrefRef.current && selectedValue) selectedValuePrefRef.current = selectedValue
|
|
71
|
+
}, [selectedValue])
|
|
45
72
|
|
|
46
73
|
const parentRelInfo = useMemo(() => {
|
|
47
74
|
if (breadcrumbs.length < 2) return { formJoins: [], formDataId: '' }
|
|
@@ -84,16 +111,27 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
84
111
|
}),
|
|
85
112
|
{},
|
|
86
113
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
|
|
115
|
+
let baseQuery: { [key: string]: any } = { DeletedDate: null }
|
|
116
|
+
if (filterRef.current && filterRef.current.type === FilterConfigTypeEnum.Custom && filterRef.current.config)
|
|
117
|
+
baseQuery = { ...baseQuery, ...JSON.parse(JSON.stringify(filterRef.current.config).replaceAll('@', '$')) }
|
|
118
|
+
|
|
119
|
+
const { request, cancel } = cancelableClient.post(`/api/report/data/${optionSource.formId}`, {
|
|
120
|
+
joins: filteredJoins,
|
|
121
|
+
match: JSON.stringify(
|
|
122
|
+
parentRelInfo.formDataId && !isManyToManyPageRef.current
|
|
123
|
+
? { ...baseQuery, ...getIdEqualsQuery(lastJoin?.alias, parentRelInfo.formDataId) }
|
|
124
|
+
: { ...baseQuery },
|
|
125
|
+
),
|
|
126
|
+
project: JSON.stringify({
|
|
127
|
+
value: '$_id',
|
|
128
|
+
label: `$${optionSource.field}`,
|
|
129
|
+
...dynamicFormProjectOtherFields,
|
|
130
|
+
}),
|
|
131
|
+
})
|
|
132
|
+
cancelReqFuncRef.current = { ...cancelReqFuncRef.current, otherForm: cancel }
|
|
133
|
+
|
|
134
|
+
request
|
|
97
135
|
.then((res) => {
|
|
98
136
|
if (res.status === 200) {
|
|
99
137
|
const { totalRecords, data } = res.data
|
|
@@ -108,30 +146,65 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
108
146
|
|
|
109
147
|
const fetchLinkedFormData = useCallback(
|
|
110
148
|
(optionSource: IOptionSourceLinkedForm) => {
|
|
111
|
-
if (!optionSource.baseFormId) {
|
|
149
|
+
if (!optionSource.baseFormId || (!!dependentOnRef.current && !dependentFieldValue)) {
|
|
112
150
|
setLoading(false)
|
|
113
151
|
return
|
|
114
152
|
}
|
|
115
153
|
|
|
116
|
-
|
|
154
|
+
if (!isNewFormDataPage(formDataId) && optionSource.joins.length > 2 && selectedValuePrefRef.current) {
|
|
155
|
+
const { request, cancel } = cancelableClient.get(
|
|
156
|
+
`/api/formdata/${optionSource.baseFormId}/${selectedValuePrefRef.current}`,
|
|
157
|
+
)
|
|
158
|
+
cancelReqFuncRef.current = { ...cancelReqFuncRef.current, linkedFormTemp: cancel }
|
|
159
|
+
|
|
160
|
+
request
|
|
161
|
+
.then((res) => {
|
|
162
|
+
if (res.status === 200) {
|
|
163
|
+
let parsedFormData = JSON.parse(res.data.data)
|
|
164
|
+
if (parsedFormData)
|
|
165
|
+
setOptions((c) =>
|
|
166
|
+
c.length > 0
|
|
167
|
+
? c
|
|
168
|
+
: [
|
|
169
|
+
{
|
|
170
|
+
value: selectedValuePrefRef.current!,
|
|
171
|
+
label: parsedFormData[optionSource.field.split('Data.')[1]],
|
|
172
|
+
},
|
|
173
|
+
{ value: 'PLACEHOLDER', label: loadingOptionLabel, disabled: true },
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.finally(() => setLoading(false))
|
|
179
|
+
}
|
|
117
180
|
|
|
181
|
+
const { groupOtherFields, projectOtherFields } = getOtherFields(optionSource)
|
|
118
182
|
const groupFields: { [key: string]: string | { $first: any } } = {
|
|
119
183
|
_id: '$_id',
|
|
120
184
|
label: { $first: `$${optionSource.field}` },
|
|
121
185
|
...groupOtherFields,
|
|
122
186
|
}
|
|
187
|
+
let baseQuery: { [key: string]: any } = { DeletedDate: null }
|
|
188
|
+
if (filterRef.current && filterRef.current.type === FilterConfigTypeEnum.Custom && filterRef.current.config)
|
|
189
|
+
baseQuery = { ...baseQuery, ...JSON.parse(JSON.stringify(filterRef.current.config).replaceAll('@', '$')) }
|
|
123
190
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
const { request, cancel } = cancelableClient.post(`/api/report/data/${optionSource.baseFormId}`, {
|
|
192
|
+
joins: optionSource.joins,
|
|
193
|
+
match: JSON.stringify(
|
|
194
|
+
dependentFieldValue
|
|
195
|
+
? { ...baseQuery, ...getIdEqualsQuery(dependentOnRef.current?.formId, dependentFieldValue) }
|
|
196
|
+
: parentRelInfo.formDataId
|
|
197
|
+
? { ...baseQuery, ...getIdEqualsQuery(parentRelInfo.detailPageFormId, parentRelInfo.formDataId) }
|
|
198
|
+
: !isNewFormDataPage(formDataId) && detailPageFormId
|
|
199
|
+
? { ...baseQuery, ...getIdEqualsQuery(detailPageFormId, formDataId) }
|
|
200
|
+
: baseQuery,
|
|
201
|
+
),
|
|
202
|
+
group: JSON.stringify(groupFields),
|
|
203
|
+
project: JSON.stringify({ _id: 0, value: '$_id', label: 1, ...projectOtherFields }),
|
|
204
|
+
})
|
|
205
|
+
cancelReqFuncRef.current = { ...cancelReqFuncRef.current, linkedForm: cancel }
|
|
206
|
+
|
|
207
|
+
request
|
|
135
208
|
.then((res) => {
|
|
136
209
|
if (res.status === 200) {
|
|
137
210
|
const { totalRecords, data } = res.data
|
|
@@ -143,7 +216,7 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
143
216
|
})
|
|
144
217
|
.finally(() => setLoading(false))
|
|
145
218
|
},
|
|
146
|
-
[parentRelInfo, formDataId,
|
|
219
|
+
[parentRelInfo, formDataId, detailPageFormId, dependentFieldValue],
|
|
147
220
|
)
|
|
148
221
|
|
|
149
222
|
const fetchDetailsStaticOptions = useCallback((optionSource: IOptionSourceReadFromDetails) => {
|
|
@@ -153,12 +226,12 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
153
226
|
return
|
|
154
227
|
}
|
|
155
228
|
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
229
|
+
const { request, cancel } = cancelableClient.post(`/api/form/${formId}`, {
|
|
230
|
+
project: JSON.stringify({ [optionFieldPath]: 1 }),
|
|
231
|
+
})
|
|
232
|
+
cancelReqFuncRef.current = { ...cancelReqFuncRef.current, staticOptions: cancel }
|
|
233
|
+
|
|
234
|
+
request
|
|
162
235
|
.then((res) => {
|
|
163
236
|
if (res.status === 200) {
|
|
164
237
|
const resData = res.data
|
|
@@ -178,9 +251,6 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
178
251
|
useEffect(() => {
|
|
179
252
|
if (!props.optionSource || inPreviewMode === undefined) return
|
|
180
253
|
|
|
181
|
-
const axiosSource = clientCancelToken.source()
|
|
182
|
-
cancelTokenRef.current = axiosSource.token
|
|
183
|
-
|
|
184
254
|
if (props.optionSource.type === FieldElementOptionSourceEnum.Static) {
|
|
185
255
|
setOptions(props.optionSource.options.map((op: IFormLayoutFieldOption) => ({ value: op.id, label: op.value })))
|
|
186
256
|
setLoading(false)
|
|
@@ -201,10 +271,17 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
201
271
|
return
|
|
202
272
|
}
|
|
203
273
|
} else setLoading(false)
|
|
204
|
-
|
|
205
|
-
return () => axiosSource.cancel()
|
|
206
274
|
}, [props.optionSource, inPreviewMode, fetchLinkedFormData, fetchFormData])
|
|
207
275
|
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
return () => {
|
|
278
|
+
cancelReqFuncRef.current.otherForm?.()
|
|
279
|
+
cancelReqFuncRef.current.linkedForm?.()
|
|
280
|
+
cancelReqFuncRef.current.linkedFormTemp?.()
|
|
281
|
+
cancelReqFuncRef.current.staticOptions?.()
|
|
282
|
+
}
|
|
283
|
+
}, [])
|
|
284
|
+
|
|
208
285
|
return (
|
|
209
286
|
<div className="relative">
|
|
210
287
|
<LayoutRenderer_FieldElement
|
|
@@ -225,7 +302,7 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
225
302
|
const formInfo = getFormById(props.optionSource.formId)
|
|
226
303
|
if (!formInfo) return
|
|
227
304
|
|
|
228
|
-
push({ label:
|
|
305
|
+
push({ label: formInfo.name, href: location.pathname, type: BreadcrumbTypeEnum.Details })
|
|
229
306
|
navigate(`${constructDynamicFormHref(formInfo.name)}/${selectedValue}`)
|
|
230
307
|
}}
|
|
231
308
|
>
|
|
@@ -285,3 +362,10 @@ const getOtherFields = (optionSource: IOptionSourceLinkedForm | IOptionSourceLin
|
|
|
285
362
|
|
|
286
363
|
type IOtherField = 'field2' | 'field3' | 'field4'
|
|
287
364
|
type IOTherField_RC = 'field1_RC' | 'field2_RC' | 'field3_RC' | 'field4_RC'
|
|
365
|
+
|
|
366
|
+
const loadingOptionLabel = (
|
|
367
|
+
<div className="flex items-center gap-2">
|
|
368
|
+
<Spin size="small" />
|
|
369
|
+
<span className="italic">Fetching rest of the data..</span>
|
|
370
|
+
</div>
|
|
371
|
+
)
|
|
@@ -5,7 +5,7 @@ import FormDataListSkeleton_Table from '../../../common/loading-skeletons/table'
|
|
|
5
5
|
import { getIdEqualsQuery, getProjectionKey, isNewFormDataPage, mergeJoins } from '../../../../functions'
|
|
6
6
|
import { cancelableClient } from '../../../../api/client'
|
|
7
7
|
import { IFormContext } from '../1-row'
|
|
8
|
-
import { FormRelationshipEnum } from '../../../../enums'
|
|
8
|
+
import { FilterConfigTypeEnum, FormRelationshipEnum } from '../../../../enums'
|
|
9
9
|
import {
|
|
10
10
|
IFormDataListData,
|
|
11
11
|
IFormDataLoadElementProps,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
export default function LayoutRenderer_LoadFormData({ formContext, elementProps }: ILayoutRenderer_LoadFormData) {
|
|
19
19
|
const { formId, formRef, formDataId } = formContext
|
|
20
|
-
const { joins, baseFormId, m2mRelFormId, displayConfigId } = elementProps
|
|
20
|
+
const { joins, baseFormId, m2mRelFormId, displayConfigId, filter: initialFilter } = elementProps
|
|
21
21
|
const lastChildInfo = useRef<{ formName: string; parentFormJoins: IFormJoin[] }>({
|
|
22
22
|
formName: '',
|
|
23
23
|
parentFormJoins: [],
|
|
@@ -32,7 +32,11 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
32
32
|
const m2mForeignKeysRef = useRef<{ currentForm: string; otherForm: string } | undefined>()
|
|
33
33
|
|
|
34
34
|
const fetchDataList = useCallback(
|
|
35
|
-
(
|
|
35
|
+
(
|
|
36
|
+
headerAppliedFilters: string = initialFilter && initialFilter.type === FilterConfigTypeEnum.Custom
|
|
37
|
+
? JSON.stringify(initialFilter.config).replaceAll('@', '$')
|
|
38
|
+
: '',
|
|
39
|
+
) => {
|
|
36
40
|
if (!baseFormId) return
|
|
37
41
|
setLoadings((c) => ({ ...c, data: true }))
|
|
38
42
|
|
|
@@ -42,12 +46,12 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
42
46
|
const { request, cancel } = cancelableClient.post(`/api/report/data/${baseFormId}`, {
|
|
43
47
|
joins: mergeJoins([joins, formJoinsRef.current]),
|
|
44
48
|
match: matchFilters.length
|
|
45
|
-
? JSON.stringify(matchFilters.length === 1 ? matchFilters[0] : { $and: matchFilters })
|
|
49
|
+
? JSON.stringify(matchFilters.length === 1 ? matchFilters[0] : { $and: matchFilters }).replaceAll(
|
|
50
|
+
`${baseFormId}.`,
|
|
51
|
+
'',
|
|
52
|
+
)
|
|
46
53
|
: '',
|
|
47
|
-
project: JSON.stringify(
|
|
48
|
-
...dataProjectRef.current,
|
|
49
|
-
...joins.reduce((curr, j) => ({ ...curr, [`${j.formId}_id`]: `$${j.formId}._id` }), {}),
|
|
50
|
-
}),
|
|
54
|
+
project: JSON.stringify(dataProjectRef.current),
|
|
51
55
|
})
|
|
52
56
|
reportDataApiCancelFuncRef.current = cancel
|
|
53
57
|
|
|
@@ -59,7 +63,7 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
59
63
|
})
|
|
60
64
|
.finally(() => setLoadings({ initial: false, data: false }))
|
|
61
65
|
},
|
|
62
|
-
[joins, baseFormId],
|
|
66
|
+
[joins, baseFormId, initialFilter],
|
|
63
67
|
)
|
|
64
68
|
|
|
65
69
|
useEffect(() => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
IDndLayoutElement,
|
|
23
23
|
IFormLayoutElementConditions,
|
|
24
24
|
IReadFieldDataElementProps,
|
|
25
|
+
IBreadcrumbElementProps,
|
|
25
26
|
} from '../../../../types'
|
|
26
27
|
|
|
27
28
|
function LayoutRendererElementWrapper({
|
|
@@ -175,7 +176,7 @@ function LayoutRendererElement({
|
|
|
175
176
|
return <LayoutRenderer_CurrencyField elementData={elementData} formContext={formContext} {...elementBaseProps} />
|
|
176
177
|
|
|
177
178
|
case ElementTypeEnum.Breadcrumb:
|
|
178
|
-
return <LayoutRenderer_Breadcrumb />
|
|
179
|
+
return <LayoutRenderer_Breadcrumb elementProps={props as IBreadcrumbElementProps} />
|
|
179
180
|
|
|
180
181
|
case ElementTypeEnum.Placeholder:
|
|
181
182
|
return <div />
|
|
@@ -135,7 +135,7 @@ export default function ReportFilterModal({ reportId, closeModal }: IReportFilte
|
|
|
135
135
|
]}
|
|
136
136
|
>
|
|
137
137
|
<Spin spinning={loadings.converting}>
|
|
138
|
-
<Form name="generate_report_form" onFinish={(
|
|
138
|
+
<Form name="generate_report_form" onFinish={() => {}} layout="vertical">
|
|
139
139
|
Filters
|
|
140
140
|
</Form>
|
|
141
141
|
</Spin>
|
|
@@ -37,7 +37,7 @@ export default function ReportListComponent() {
|
|
|
37
37
|
{reports.map((r) => (
|
|
38
38
|
<div className="flex items-center justify-between" key={r.id}>
|
|
39
39
|
<span>{r.name}</span>
|
|
40
|
-
<Button_FillerPortal primary onClick={() =>
|
|
40
|
+
<Button_FillerPortal primary onClick={() => {}}>
|
|
41
41
|
Generate
|
|
42
42
|
</Button_FillerPortal>
|
|
43
43
|
</div>
|
package/src/enums/form.enum.ts
CHANGED
|
@@ -53,6 +53,7 @@ export const renderTableColumns = (
|
|
|
53
53
|
? 'justify-end'
|
|
54
54
|
: ''
|
|
55
55
|
}`}
|
|
56
|
+
onClick={(e) => e.stopPropagation()}
|
|
56
57
|
>
|
|
57
58
|
{(el.renderConfig as IDataRender_Buttons).buttons.length > 1 ? (
|
|
58
59
|
<Dropdown
|
|
@@ -125,6 +126,9 @@ export const renderData = (data?: any, renderConfig?: IDataRenderConfig): ReactN
|
|
|
125
126
|
|
|
126
127
|
const renderDateData = (data: string, renderConfig: IDataRender_Date) => {
|
|
127
128
|
const { format = '' } = (renderConfig as IDataRender_Date) ?? {}
|
|
129
|
+
|
|
130
|
+
if (renderConfig.displayInOriginalTz) return dayjs(data).utc().format(format)
|
|
131
|
+
|
|
128
132
|
return dayjs(data).format(format)
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import dayjs from 'dayjs'
|
|
2
1
|
import { BSON_DATA_IDENTIFIER_PREFIXES } from '../../constants'
|
|
3
2
|
import { isValidMongoDbId } from '..'
|
|
3
|
+
import dayjs from 'dayjs'
|
|
4
4
|
|
|
5
5
|
export function toMongoDbExtendedJSON(input: Record<string, any>): Record<string, any> {
|
|
6
6
|
const out: Record<string, any> = {}
|
|
@@ -13,8 +13,7 @@ export function toMongoDbExtendedJSON(input: Record<string, any>): Record<string
|
|
|
13
13
|
out[key] = val
|
|
14
14
|
}
|
|
15
15
|
} else if (key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Date)) {
|
|
16
|
-
|
|
17
|
-
if (!isNaN(Date.parse(s))) out[key] = { $date: s }
|
|
16
|
+
if (dayjs(val).isValid()) out[key] = { $date: dayjs(val).utc().toISOString() }
|
|
18
17
|
else {
|
|
19
18
|
console.warn(`Invalid Date for "${key}":`, val)
|
|
20
19
|
out[key] = val
|
|
@@ -32,7 +31,7 @@ export function toMongoDbExtendedJSON(input: Record<string, any>): Record<string
|
|
|
32
31
|
return out
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
export function fromMongoDbExtendedJSON(input: Record<string, any
|
|
34
|
+
export function fromMongoDbExtendedJSON(input: Record<string, any>, originalTzFields: string[]): Record<string, any> {
|
|
36
35
|
const out: Record<string, any> = {}
|
|
37
36
|
|
|
38
37
|
for (const [key, val] of Object.entries(input)) {
|
|
@@ -40,7 +39,7 @@ export function fromMongoDbExtendedJSON(input: Record<string, any>): Record<stri
|
|
|
40
39
|
out[key] = (val as any).$oid
|
|
41
40
|
} else if (key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Date) && val && typeof val === 'object' && '$date' in val) {
|
|
42
41
|
const date = (val as any).$date
|
|
43
|
-
out[key] = date ? dayjs(date) : date
|
|
42
|
+
out[key] = date ? (originalTzFields.includes(key) ? dayjs(date).utc() : dayjs(date)) : date
|
|
44
43
|
} else if (
|
|
45
44
|
key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Number) &&
|
|
46
45
|
val &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ButtonElementSizeEnum, ButtonElementTypeEnum } from '../../enums'
|
|
2
|
-
import { IButtonElementProps } from '../../types'
|
|
1
|
+
import { ButtonElementSizeEnum, ButtonElementTypeEnum, ElementTypeEnum } from '../../enums'
|
|
2
|
+
import { IButtonElementProps, IDndLayoutElement, IPickerElementProps } from '../../types'
|
|
3
3
|
|
|
4
4
|
// since all the field elements have different type, this function is generalizing the types
|
|
5
5
|
export const getElementGeneralizedProps = (props: Record<string, any> | undefined): Record<string, any> => ({
|
|
@@ -18,3 +18,14 @@ export const getButtonRenderProps = (btnProps: IButtonElementProps) => ({
|
|
|
18
18
|
ButtonElementTypeEnum.DangerOutlined,
|
|
19
19
|
].includes(btnProps.buttonType!),
|
|
20
20
|
})
|
|
21
|
+
|
|
22
|
+
export const getPickerFieldsWithOriginalTz = (elements: { [key: string]: IDndLayoutElement }) =>
|
|
23
|
+
Object.values(elements).reduce((curr: string[], el) => {
|
|
24
|
+
if (
|
|
25
|
+
[ElementTypeEnum.DatePicker, ElementTypeEnum.TimePicker].includes(el.elementType) &&
|
|
26
|
+
(el.props as IPickerElementProps)?.displayInOriginalTz
|
|
27
|
+
)
|
|
28
|
+
return [...curr, el.key]
|
|
29
|
+
|
|
30
|
+
return curr
|
|
31
|
+
}, [])
|
|
@@ -9,6 +9,7 @@ import * as FaIcons from 'react-icons/fa'
|
|
|
9
9
|
import { cookieHandler } from '../../../../functions/cookie-handler'
|
|
10
10
|
import { UserAuth } from '../../../../api/user'
|
|
11
11
|
import { useBreadcrumb } from '../../../../components/common/custom-hooks/use-breadcrumb.hook'
|
|
12
|
+
import { BreadcrumbTypeEnum } from '../../../../enums'
|
|
12
13
|
|
|
13
14
|
const { Header, Content, Sider } = Layout
|
|
14
15
|
|
|
@@ -491,8 +492,8 @@ export const layoutTemplates = [
|
|
|
491
492
|
onClick={() => {
|
|
492
493
|
reset()
|
|
493
494
|
const splittedFormName = form.name.split(' ')?.[0] ?? ''
|
|
494
|
-
push({ label:
|
|
495
|
-
|
|
495
|
+
push({ label: splittedFormName, href, type: BreadcrumbTypeEnum.List })
|
|
496
|
+
|
|
496
497
|
if (isParentMenu) toggleMenu(form.id)
|
|
497
498
|
}}
|
|
498
499
|
style={{
|
package/src/types/forms/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface IFormDataListConfig {
|
|
|
24
24
|
pagination?: IFormDataListPagination
|
|
25
25
|
title?: string
|
|
26
26
|
showCount?: boolean
|
|
27
|
-
|
|
27
|
+
onRowClickToNavigate?: boolean
|
|
28
28
|
defaultFilter?: { [key: string]: any } | null
|
|
29
29
|
defaultSorter?: IDataListSorter
|
|
30
30
|
}
|
|
@@ -112,46 +112,52 @@ export interface ICurrencyInputElementProps {
|
|
|
112
112
|
|
|
113
113
|
export interface ISelectElement extends BaseFormLayoutElement {
|
|
114
114
|
elementType: ElementTypeEnum.Select
|
|
115
|
-
props:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
props: ISelectElementProps
|
|
116
|
+
}
|
|
117
|
+
export interface IOptionDependentOn {
|
|
118
|
+
formId: number
|
|
119
|
+
field: string
|
|
120
|
+
}
|
|
121
|
+
interface IOptionElementProps_Base {
|
|
122
|
+
label?: string
|
|
123
|
+
dependentOn?: IOptionDependentOn
|
|
124
|
+
description?: string
|
|
125
|
+
hasNoLabel?: boolean
|
|
126
|
+
optionSource?: IFieldElementOptionSource
|
|
127
|
+
filter?: IFilterConfig
|
|
128
|
+
defaultValue?: string | number | boolean
|
|
129
|
+
}
|
|
130
|
+
export type ISelectElementProps = IOptionElementProps_Base & {
|
|
131
|
+
placeholder?: string
|
|
132
|
+
allowClear?: boolean
|
|
133
|
+
defaultValue?: string | number | boolean
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
/** -------------------------------------------------------------------------------------------- */
|
|
128
137
|
|
|
129
138
|
export interface IRadioElement extends BaseFormLayoutElement {
|
|
130
139
|
elementType: ElementTypeEnum.Radio
|
|
131
|
-
props:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
optionSource?: IFieldElementOptionSource
|
|
137
|
-
defaultValue?: string | number | boolean
|
|
138
|
-
filter?: IFilterConfig
|
|
139
|
-
isCustom?: boolean
|
|
140
|
-
}
|
|
140
|
+
props: IRadioElementProps
|
|
141
|
+
}
|
|
142
|
+
export type IRadioElementProps = IOptionElementProps_Base & {
|
|
143
|
+
isMultiValue?: boolean
|
|
144
|
+
isCustom?: boolean
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
/** -------------------------------------------------------------------------------------------- */
|
|
144
148
|
|
|
145
149
|
export interface IPickerElement extends BaseFormLayoutElement {
|
|
146
150
|
elementType: ElementTypeEnum.DatePicker | ElementTypeEnum.TimePicker
|
|
147
|
-
props:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
props: IPickerElementProps
|
|
152
|
+
}
|
|
153
|
+
export interface IPickerElementProps {
|
|
154
|
+
label?: string
|
|
155
|
+
hasNoLabel?: boolean
|
|
156
|
+
placeholder?: string
|
|
157
|
+
description?: string
|
|
158
|
+
allowClear?: boolean
|
|
159
|
+
filter?: IFilterConfig
|
|
160
|
+
displayInOriginalTz?: boolean
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
/** -------------------------------------------------------------------------------------------- */
|
|
@@ -338,6 +344,13 @@ export interface IImageElement extends BaseFormLayoutElement {
|
|
|
338
344
|
|
|
339
345
|
export interface IBreadcrumbElement extends BaseFormLayoutElement {
|
|
340
346
|
elementType: ElementTypeEnum.Breadcrumb
|
|
347
|
+
props: IBreadcrumbElementProps
|
|
348
|
+
}
|
|
349
|
+
export interface IBreadcrumbElementProps {
|
|
350
|
+
hasNoLabel?: boolean
|
|
351
|
+
detailText?: string
|
|
352
|
+
listText?: string
|
|
353
|
+
newText?: string
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
/** -------------------------------------------------------------------------------------------- */
|