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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/src/api/client.ts +6 -2
  3. package/src/components/common/custom-hooks/use-breadcrumb.hook.ts +7 -4
  4. package/src/components/common/custom-hooks/use-dayjs-extender.hook.ts +2 -0
  5. package/src/components/common/custom-hooks/use-login-handler.ts +143 -147
  6. package/src/components/companies/2-unauthenticated/index.tsx +0 -1
  7. package/src/components/form/1-list/index.tsx +2 -8
  8. package/src/components/form/1-list/table.tsx +15 -2
  9. package/src/components/form/2-details/index.tsx +17 -5
  10. package/src/components/form/layout-renderer/1-row/index.tsx +2 -0
  11. package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +4 -4
  12. package/src/components/form/layout-renderer/3-element/11-breadcrumb.tsx +12 -2
  13. package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +131 -47
  14. package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +13 -9
  15. package/src/components/form/layout-renderer/3-element/index.tsx +2 -1
  16. package/src/components/modals/report-filters.modal/index.tsx +1 -1
  17. package/src/components/report/1-list/index.tsx +1 -1
  18. package/src/enums/form.enum.ts +5 -0
  19. package/src/functions/forms/data-render-functions.tsx +4 -0
  20. package/src/functions/forms/extended-json-handlers.ts +4 -5
  21. package/src/functions/forms/get-element-props.ts +13 -2
  22. package/src/types/companies/site-layout/authenticated/index.tsx +3 -2
  23. package/src/types/forms/data-list/index.ts +1 -0
  24. package/src/types/forms/index.ts +1 -1
  25. package/src/types/forms/layout-elements/data-render-config.ts +1 -0
  26. package/src/types/forms/layout-elements/index.ts +41 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.7.10-dev.2",
3
+ "version": "1.7.10-dev.3",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
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({ label: splittedFormName, href: constructDynamicFormHref(firstForm.name) })
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
- if (crumbs[i].href !== crumbs[i - 1].href) {
40
- result.push(crumbs[i])
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
- const navigate = useNavigate()
9
- const domain = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.Domain) || ''
10
- const [loading, updateLoading] = useReducer(
11
- (currState: any, updated: any) => ({ ...currState, ...updated }),
12
- { login: false }
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
- const [googleAuthSetup, setGoogleAuthSetup] = useState<null | {
16
- qrCodeImageUrl: string
17
- manualEntryCode: string
18
- key: string
19
- }>(null)
14
+ const [googleAuthSetup, setGoogleAuthSetup] = useState<null | {
15
+ qrCodeImageUrl: string
16
+ manualEntryCode: string
17
+ key: string
18
+ }>(null)
20
19
 
21
- const [showVerificationModal, setShowVerificationModal] = useState(false)
22
- const [latestLoginValues, setLatestLoginValues] = useState<any>(null)
20
+ const [showVerificationModal, setShowVerificationModal] = useState(false)
21
+ const [latestLoginValues, setLatestLoginValues] = useState<any>(null)
23
22
 
24
- const handleLogin = async (values: any) => {
25
- updateLoading({ login: true })
26
- setLatestLoginValues(values)
23
+ const handleLogin = async (values: any) => {
24
+ updateLoading({ login: true })
25
+ setLatestLoginValues(values)
27
26
 
28
- const { authRes, initialPath } = await auth(values.email, values.password, domain, values.type || 'password')
29
- if (authRes.status === 200) {
30
- const data = authRes.data
31
- if (!data.required2FA) {
32
- navigate(initialPath)
33
- } else if (data.required2FA && !data.configuredGoogleAuth) {
34
- const googleSetup = await client.get(`api/authorization/authkey`)
35
- if (googleSetup.status === 200) {
36
- setGoogleAuthSetup({
37
- qrCodeImageUrl: googleSetup.data.qrCodeImageUrl,
38
- manualEntryCode: googleSetup.data.manualEntrySetupCode,
39
- key: googleSetup.data.key,
40
- })
41
- }
42
- } else if (data.required2FA && data.configuredGoogleAuth) {
43
- setShowVerificationModal(true)
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
- updateLoading({ login: false })
46
+ } else if (data.required2FA && data.configuredGoogleAuth) {
47
+ setShowVerificationModal(true)
48
+ }
48
49
  }
49
50
 
50
- const handleSetupDone = async () => {
51
- if (!googleAuthSetup) return
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
- const handleVerifyCode = async (code: any) => {
62
- await handleLogin({ email: latestLoginValues.email, password: code, type: 'refresh_token' })
63
- }
64
- const handleUnlinkAuthenticator = async () => {
65
- const res = await client.get('api/authorization/authkey')
66
- if (res.status === 200) {
67
- setGoogleAuthSetup({
68
- qrCodeImageUrl: res.data.qrCodeImageUrl,
69
- manualEntryCode: res.data.manualEntrySetupCode,
70
- key: res.data.key,
71
- })
72
- setShowVerificationModal(false)
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
- const handleForgotPassword = async (email?: string) => {
77
- if (!email) {
78
- return message.error('Please enter your email or username first.')
79
- }
80
- const payload = {
81
- subject: 'Reset your password',
82
- body: `
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('forgot-password', 'reset-password')}?token=[token]&domain=${domain}&email=${email}"
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
- from: 'no-reply@icodice.com',
101
- to: email,
102
- cc: '',
103
- bcc: '',
104
- notificationId: 0,
105
- attachments: [],
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
- 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
-
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
- 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
- }
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
- return {
174
- handleLogin,
175
- loading,
176
- googleAuthSetup,
177
- showVerificationModal,
178
- handleSetupDone,
179
- setShowVerificationModal,
180
- handleSetupCancel,
181
- handleVerifyCode,
182
- handleUnlinkAuthenticator,
183
- handleForgotPassword,
184
- handleVerifyToken,
185
- handleResetPassword
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) ? `New ${splittedFormName.toLowerCase()}` : `${splittedFormName} details`,
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(parsedFormData)
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
 
@@ -69,6 +69,8 @@ export const LayoutRendererRow = memo(
69
69
  hiddenColIdx.current = [...hiddenColIdx.current, colIdx]
70
70
  return c + 1
71
71
  } else {
72
+ hiddenColIdx.current = hiddenColIdx.current.filter((idx) => idx !== colIdx)
73
+
72
74
  if (c - 1 < 0) return 0
73
75
  return c - 1
74
76
  }
@@ -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">{bc.label}</span>
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 client, { clientCancelToken } from '../../../../api/client'
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
- const { formRef, formId, formDataId } = formContext
35
- const [options, setOptions] = useState<{ value: string; label: string }[]>([])
36
- const props = getElementGeneralizedProps(elementData.props)
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
- const { inPreviewMode } = useFormPreservedItemValues(formRef)
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
- client
88
- .post(`/api/report/data/${optionSource.formId}`, {
89
- joins: filteredJoins,
90
- match: JSON.stringify(
91
- parentRelInfo.formDataId && !isManyToManyPageRef.current
92
- ? { DeletedDate: null, ...getIdEqualsQuery(lastJoin?.alias, parentRelInfo.formDataId) }
93
- : { DeletedDate: null },
94
- ),
95
- project: JSON.stringify({ value: '$_id', label: `$${optionSource.field}`, ...dynamicFormProjectOtherFields }),
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
- const { groupOtherFields, projectOtherFields } = getOtherFields(optionSource)
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
- client
125
- .post(`/api/report/data/${optionSource.baseFormId}`, {
126
- joins: optionSource.joins,
127
- match: JSON.stringify(
128
- parentRelInfo.formDataId
129
- ? { ...getIdEqualsQuery(parentRelInfo.detailPageFormId, parentRelInfo.formDataId), DeletedDate: null }
130
- : { DeletedDate: null },
131
- ),
132
- group: JSON.stringify(groupFields),
133
- project: JSON.stringify({ _id: 0, value: '$_id', label: 1, ...projectOtherFields }),
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, formId],
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
- client
157
- .post(
158
- `/api/form/${formId}`,
159
- { project: JSON.stringify({ [optionFieldPath]: 1 }) },
160
- { cancelToken: cancelTokenRef.current },
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: `${formInfo.name} details`, href: location.pathname })
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
- (headerAppliedFilters?: string) => {
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={(values) => console.log({ values })} layout="vertical">
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={() => console.log(r)}>
40
+ <Button_FillerPortal primary onClick={() => {}}>
41
41
  Generate
42
42
  </Button_FillerPortal>
43
43
  </div>
@@ -198,3 +198,8 @@ export enum ReadFieldDataValueTypeEnum {
198
198
  Default = 'Default',
199
199
  Evaluated = 'Evaluated',
200
200
  }
201
+ export enum BreadcrumbTypeEnum {
202
+ Details = 1,
203
+ List,
204
+ New,
205
+ }
@@ -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
- const s = String(val)
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>): 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: `${splittedFormName} list`, href })
495
-
495
+ push({ label: splittedFormName, href, type: BreadcrumbTypeEnum.List })
496
+
496
497
  if (isParentMenu) toggleMenu(form.id)
497
498
  }}
498
499
  style={{
@@ -10,6 +10,7 @@ export interface IFormDataListColumn {
10
10
  label: string
11
11
  order: number
12
12
  optionFieldPath?: string
13
+ displayInOriginalTz?: boolean
13
14
  sortable?: boolean
14
15
  align?: AlignTypeEnum
15
16
  width?: string | number
@@ -24,7 +24,7 @@ export interface IFormDataListConfig {
24
24
  pagination?: IFormDataListPagination
25
25
  title?: string
26
26
  showCount?: boolean
27
- usePopup?: boolean
27
+ onRowClickToNavigate?: boolean
28
28
  defaultFilter?: { [key: string]: any } | null
29
29
  defaultSorter?: IDataListSorter
30
30
  }
@@ -45,6 +45,7 @@ export interface IDataRender_Image {
45
45
  export interface IDataRender_Date {
46
46
  type: DataRenderTypeEnum.Date
47
47
  format: string
48
+ displayInOriginalTz?: boolean
48
49
  }
49
50
 
50
51
  export interface IDataRender_Conditional {
@@ -112,46 +112,52 @@ export interface ICurrencyInputElementProps {
112
112
 
113
113
  export interface ISelectElement extends BaseFormLayoutElement {
114
114
  elementType: ElementTypeEnum.Select
115
- props: {
116
- label?: string
117
- placeholder?: string
118
- description?: string
119
- hasNoLabel?: boolean
120
- allowClear?: boolean
121
- optionSource?: IFieldElementOptionSource
122
- defaultValue?: string | number | boolean
123
- filter?: IFilterConfig
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
- label?: string
133
- description?: string
134
- hasNoLabel?: boolean
135
- isMultiValue?: boolean
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
- label?: string
149
- hasNoLabel?: boolean
150
- placeholder?: string
151
- description?: string
152
- allowClear?: boolean
153
- filter?: IFilterConfig
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
  /** -------------------------------------------------------------------------------------------- */