form-craft-package 1.7.11-dev.0 → 1.7.12-dev.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.7.11-dev.0",
3
+ "version": "1.7.12-dev.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -11,10 +11,13 @@
11
11
  "license": "ISC",
12
12
  "description": "",
13
13
  "dependencies": {
14
+ "@tanstack/query-sync-storage-persister": "^5.77.2",
15
+ "@tanstack/react-query-persist-client": "^5.77.2",
14
16
  "ajv": "^8.17.1",
15
17
  "axios": "^1.9.0",
16
18
  "crypto-js": "^4.2.0",
17
19
  "js-cookie": "^3.0.5",
20
+ "lodash.isequal": "^4.5.0",
18
21
  "pdfmake": "^0.2.18",
19
22
  "qs": "^6.14.0",
20
23
  "quill": "^2.0.3",
@@ -24,6 +27,7 @@
24
27
  "react-signature-canvas": "^1.0.7"
25
28
  },
26
29
  "peerDependencies": {
30
+ "@tanstack/react-query": "^5.76.1",
27
31
  "antd": ">=5.21.6",
28
32
  "dayjs": ">=1.11.13",
29
33
  "react": ">=18.0.0 <19.0.0",
@@ -34,6 +38,7 @@
34
38
  "devDependencies": {
35
39
  "@types/crypto-js": "^4.2.2",
36
40
  "@types/js-cookie": "^3.0.6",
41
+ "@types/lodash.isequal": "^4.5.8",
37
42
  "@types/pdfmake": "^0.2.11",
38
43
  "@types/qs": "^6.9.18",
39
44
  "@types/react": "^18.3.18",
package/src/api/client.ts CHANGED
@@ -10,7 +10,7 @@ import axios, {
10
10
  InternalAxiosRequestConfig,
11
11
  } from 'axios'
12
12
 
13
- import { BreadcrumbTypeEnum, LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS } from '../enums'
13
+ import { PageViewTypEnum, LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS } from '../enums'
14
14
  import { IDynamicForm } from '../types'
15
15
  import { constructDynamicFormHref, fetchDynamicForms } from '../functions'
16
16
  import { CLIENT_ID, CLIENT_SECRET } from '../constants'
@@ -27,7 +27,12 @@ apiClient.interceptors.request.use((config) => {
27
27
  return config
28
28
  })
29
29
 
30
- const authResHandler = (resData: { access_token: string; refresh_token: string; expires_in: number }) => {
30
+ const authResHandler = (resData: {
31
+ access_token: string
32
+ refresh_token: string
33
+ companyKey: string
34
+ expires_in: number
35
+ }) => {
31
36
  const expiry = new Date(Date.now() + resData.expires_in * 1000)
32
37
  Cookies.set(SHARED_COOKIE_KEYS.AccessToken, resData.access_token, {
33
38
  expires: expiry,
@@ -37,6 +42,7 @@ const authResHandler = (resData: { access_token: string; refresh_token: string;
37
42
  expires: expiry,
38
43
  path: '/',
39
44
  })
45
+ Cookies.set(SHARED_COOKIE_KEYS.CompanyKey, resData.companyKey, { expires: expiry, path: '/' })
40
46
  }
41
47
 
42
48
  let isRefreshing = false
@@ -180,7 +186,7 @@ export const auth = async (
180
186
  breadcrumbStore.push({
181
187
  label: splittedFormName,
182
188
  href: constructDynamicFormHref(firstForm.name),
183
- type: BreadcrumbTypeEnum.List,
189
+ type: PageViewTypEnum.List,
184
190
  })
185
191
  }
186
192
 
@@ -1,8 +1,8 @@
1
1
  import { useSyncExternalStore } from 'react'
2
- import { BreadcrumbTypeEnum, LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
2
+ import { PageViewTypEnum, 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; type: BreadcrumbTypeEnum }
5
+ export type IBreadcrumb = { label: string; href: string; [key: string]: any; type: PageViewTypEnum }
6
6
 
7
7
  let crumbs: IBreadcrumb[] = loadFromStorage()
8
8
  const listeners = new Set<() => void>()
@@ -0,0 +1,111 @@
1
+ import { useState, useEffect, useCallback, useMemo } from 'react'
2
+ import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query'
3
+ import isEqual from 'lodash.isequal'
4
+ import { IFormConfigDetails, IFormSchema } from '../../../types'
5
+ import client from '../../../api/client'
6
+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
7
+ import { persistQueryClient } from '@tanstack/react-query-persist-client'
8
+
9
+ export interface UseCacheFormLayoutConfig {
10
+ cachedConfig?: IFormConfigDetails & IFormSchema & { formId?: number }
11
+ isConfigLoading: boolean
12
+ isUpdateFound: boolean
13
+ applyUpdate: () => void
14
+ }
15
+
16
+ export function useCacheFormLayoutConfig(formId?: number): UseCacheFormLayoutConfig {
17
+ const queryClient = useQueryClient()
18
+
19
+ const [currentSchema, setCurrentSchema] = useState<IFormConfigDetails & IFormSchema>()
20
+ const [pendingSchema, setPendingSchema] = useState<IFormConfigDetails & IFormSchema>()
21
+
22
+ useEffect(() => {
23
+ setCurrentSchema(undefined)
24
+ setPendingSchema(undefined)
25
+ }, [formId])
26
+
27
+ const queryKey = useMemo(() => ['layoutConfig', formId ? String(formId) : 'default'] as const, [formId])
28
+
29
+ const {
30
+ data: fetchedSchema,
31
+ isLoading,
32
+ isSuccess,
33
+ } = useQuery<(IFormConfigDetails & IFormSchema) | undefined, Error>({
34
+ queryKey,
35
+ queryFn: () => fetchLayoutConfig(formId),
36
+ gcTime: Infinity,
37
+ enabled: !!formId,
38
+ placeholderData: keepPreviousData,
39
+ })
40
+
41
+ useEffect(() => {
42
+ const localStoragePersistor = createSyncStoragePersister({
43
+ storage: window.localStorage,
44
+ })
45
+ persistQueryClient({
46
+ queryClient,
47
+ persister: localStoragePersistor,
48
+ // maxAge: 1000 * 60 * 60 * 24, // 24h
49
+ dehydrateOptions: {
50
+ shouldDehydrateQuery: (query) => {
51
+ const [key, id] = query.queryKey
52
+ if (key === 'layoutConfig' && id === String(formId)) return false
53
+
54
+ return true
55
+ },
56
+ },
57
+ })
58
+ }, [queryClient, formId])
59
+
60
+ // ─── on new network data: queue diffs ───
61
+ useEffect(() => {
62
+ if (!isSuccess || !fetchedSchema || !formId) return
63
+
64
+ let timer: ReturnType<typeof setTimeout> | undefined
65
+
66
+ // first-ever load
67
+ if (!currentSchema) {
68
+ timer = setTimeout(() => {
69
+ setCurrentSchema(fetchedSchema)
70
+ }, 250)
71
+ }
72
+
73
+ // later loads with real changes → queue up
74
+ else if (!isEqual(currentSchema, fetchedSchema)) setPendingSchema(fetchedSchema)
75
+
76
+ return () => {
77
+ if (timer) clearTimeout(timer)
78
+ }
79
+ }, [isSuccess, fetchedSchema, currentSchema, formId])
80
+
81
+ // ─── apply any queued updates ──
82
+ const applyUpdate = useCallback(() => {
83
+ if (!pendingSchema) return
84
+
85
+ // overwrite the cached schema
86
+ queryClient.setQueryData(queryKey, pendingSchema)
87
+
88
+ setCurrentSchema(pendingSchema)
89
+ setPendingSchema(undefined)
90
+ }, [pendingSchema, queryClient, queryKey])
91
+
92
+ return {
93
+ cachedConfig: currentSchema?.id !== formId ? pendingSchema : currentSchema,
94
+ isConfigLoading: !!formId ? isLoading && !currentSchema : false,
95
+ isUpdateFound: Boolean(pendingSchema),
96
+ applyUpdate,
97
+ }
98
+ }
99
+
100
+ async function fetchLayoutConfig(formId?: number): Promise<(IFormConfigDetails & IFormSchema) | undefined> {
101
+ if (!formId) return undefined
102
+
103
+ try {
104
+ const res = await client.get<{ data: string }>(`/api/form/${formId}`)
105
+ if (res.status === 200) {
106
+ const { data: jsonData, ...restResData } = res.data
107
+ return { ...restResData, ...JSON.parse(jsonData) } as IFormConfigDetails & IFormSchema
108
+ }
109
+ } catch {}
110
+ return
111
+ }
@@ -3,7 +3,7 @@ import SkeletonBlock from '.'
3
3
  export default function FormDataListSkeleton_Table({ small = false }: { small?: boolean }) {
4
4
  return (
5
5
  <>
6
- <div className={`bg-white flex items-center justify-between rounded-md mb-2 ${small ? 'p-2' : 'p-3'}`}>
6
+ <div className={`flex items-center justify-between rounded-md mb-2`}>
7
7
  <SkeletonBlock width="200px" height={small ? 20 : undefined} />
8
8
  <div className="flex items-center gap-2">
9
9
  <SkeletonBlock width="150px" height={small ? 20 : undefined} />
@@ -5,22 +5,36 @@ import { ICustomFunctionCall } from '../layout-renderer/3-element/1-dynamic-butt
5
5
  import FormDataListTableComponent, { IDataListLayoutConfig } from './table'
6
6
  import { IFormJoin, IFormDataListData, IFormDataListConfig } from '../../../types'
7
7
  import { getProjectionKey, mergeJoins } from '../../../functions'
8
-
9
- function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunctionCall }: IFormDataListComponent) {
10
- const [loadings, setLoadings] = useState({ initial: true, data: true })
8
+ import { useCacheFormLayoutConfig } from '../../common/custom-hooks/use-cache-form-layout-config.hook'
9
+ import { useNotification } from '../../common/custom-hooks'
10
+
11
+ export function FormDataListComponent({
12
+ formId,
13
+ companyKey,
14
+ baseServerUrl,
15
+ onCustomFunctionCall,
16
+ }: IFormDataListComponent) {
17
+ const [dataLoading, setDataLoading] = useState(true)
11
18
  const [dataList, setDataList] = useState<{ data: IFormDataListData[]; total: number }>({ data: [], total: 0 })
12
19
  const [listLayoutConfig, setListLayoutConfig] = useState<IDataListLayoutConfig>()
20
+ const { warning, destroy } = useNotification()
13
21
 
14
22
  const reportDataApiCancelFuncRef = useRef<() => void | undefined>()
15
23
  const dataProjectRef = useRef<{ [key: string]: string }>({})
16
24
  const formJoinsRef = useRef<IFormJoin[]>([])
17
25
  const defaultFilterReqDataRef = useRef<IDataListReqData | undefined>()
26
+ const apiCallCounterRef = useRef(0)
18
27
 
19
28
  const attachmentBaseUrl = useMemo(() => `${baseServerUrl}/api/attachment/${companyKey}`, [baseServerUrl, companyKey])
20
29
  const headerLayoutContext = { formId, attachmentBaseUrl, companyKey, onCustomFunctionCall }
21
30
 
31
+ const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(formId)
32
+
33
+ useEffect(() => {
34
+ reportDataApiCancelFuncRef.current?.()
35
+ }, [formId])
36
+
22
37
  useEffect(() => {
23
- setLoadings({ initial: true, data: true })
24
38
  setDataList({ data: [], total: 0 })
25
39
  dataProjectRef.current = {}
26
40
  formJoinsRef.current = []
@@ -28,62 +42,58 @@ function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunc
28
42
  }, [location.pathname])
29
43
 
30
44
  useEffect(() => {
31
- if (!formId) return
32
- const { request, cancel } = cancelableClient.post(`/api/form/${formId}`, {
33
- project: JSON.stringify({ 'Data.dataListConfig': 1 }),
34
- })
35
- request.then(async (res) => {
36
- if (res.status === 200) {
37
- const dataListConfig: IFormDataListConfig = res.data.Data.dataListConfig
38
- const { columns, pagination, defaultFilter, defaultSorter, header } = dataListConfig
39
-
40
- if (Array.isArray(columns)) {
41
- dataProjectRef.current = columns.reduce(
42
- (curr, c) => (c.key ? { ...curr, [getProjectionKey(c.key)]: `$${c.key}` } : curr),
43
- {},
44
- )
45
-
46
- formJoinsRef.current = mergeJoins([
47
- ...columns.map((c) => c.joins),
48
- ...Object.values(header.elements).reduce((c: IFormJoin[], el) => {
49
- if (el.props && 'filter' in el.props && Array.isArray(el.props.filter?.joins)) {
50
- return [...c, el.props.filter.joins]
51
- }
52
- return c
53
- }, []),
54
- ])
55
- }
56
- const defaultReqData: IDataListReqData = {}
57
- if (!pagination?.hasNoPagination) {
58
- defaultReqData.current = 1
59
- defaultReqData.limit = pagination?.defaultPageSize ?? 10
60
- }
61
- if (defaultSorter) defaultReqData.sort = JSON.stringify({ [defaultSorter.field]: defaultSorter.order })
62
- if (defaultFilter?.config && Object.values(defaultFilter.config).length)
63
- // The operators are stored using @, and here it replaces @ with $. Otherwise, MongoDB's JSON-to-BSON conversion driver was detecting certain operators, especially $regex, and automatically modified them. To prevent data manipulation by the MongoDB driver, @ is used instead of $.
64
- defaultReqData.match = JSON.stringify(defaultFilter.config).replaceAll('@', '$')
65
-
66
- defaultFilterReqDataRef.current = defaultReqData
67
-
68
- fetchFormDataList(defaultReqData)
69
-
70
- // configForFormId is passed to ensure that the table shows the correct data list when the forms are switched quickly
71
- setListLayoutConfig({
72
- configForFormId: formId,
73
- dataListConfig: {
74
- ...dataListConfig,
75
- columns: columns.map((c) => (c.key ? { ...c, key: getProjectionKey(c.key) } : c)),
76
- },
77
- })
78
- }
45
+ if (!formId || !cachedConfig) return
46
+
47
+ const { columns, pagination, defaultFilter, defaultSorter, header } =
48
+ cachedConfig.dataListConfig as IFormDataListConfig
49
+
50
+ if (Array.isArray(columns)) {
51
+ dataProjectRef.current = columns.reduce(
52
+ (curr, c) => (c.key ? { ...curr, [getProjectionKey(c.key)]: `$${c.key}` } : curr),
53
+ {},
54
+ )
55
+
56
+ formJoinsRef.current = mergeJoins([
57
+ ...columns.map((c) => c.joins),
58
+ ...Object.values(header.elements).reduce((c: IFormJoin[], el) => {
59
+ if (el.props && 'filter' in el.props && Array.isArray(el.props.filter?.joins)) {
60
+ return [...c, el.props.filter.joins]
61
+ }
62
+ return c
63
+ }, []),
64
+ ])
65
+ }
66
+
67
+ const defaultReqData: IDataListReqData = {}
68
+
69
+ if (!pagination?.hasNoPagination) {
70
+ defaultReqData.current = 1
71
+ defaultReqData.limit = pagination?.defaultPageSize ?? 10
72
+ }
73
+ if (defaultSorter) defaultReqData.sort = JSON.stringify({ [defaultSorter.field]: defaultSorter.order })
74
+ if (defaultFilter?.config && Object.values(defaultFilter.config).length)
75
+ // The operators are stored using @, and here it replaces @ with $. Otherwise, MongoDB's JSON-to-BSON conversion driver was detecting certain operators, especially $regex, and automatically modified them. To prevent data manipulation by the MongoDB driver, @ is used instead of $.
76
+ defaultReqData.match = JSON.stringify(defaultFilter.config).replaceAll('@', '$')
77
+
78
+ defaultFilterReqDataRef.current = defaultReqData
79
+
80
+ fetchFormDataList(defaultReqData)
81
+
82
+ // configForFormId is passed to ensure that the table shows the correct data list when the forms are switched quickly
83
+ setListLayoutConfig({
84
+ configForFormId: formId,
85
+ dataListConfig: {
86
+ ...cachedConfig.dataListConfig,
87
+ columns: columns.map((c) => (c.key ? { ...c, key: getProjectionKey(c.key) } : c)),
88
+ } as IFormDataListConfig,
79
89
  })
80
-
81
- return () => cancel()
82
- }, [formId])
90
+ }, [formId, cachedConfig?.dataListConfig])
83
91
 
84
92
  const fetchFormDataList = useCallback(
85
93
  (reqData?: IDataListReqData & { skip?: number }) => {
86
94
  if (!reqData || !formId) return
95
+ apiCallCounterRef.current += 1
96
+ setDataLoading(true)
87
97
 
88
98
  if (!reqData.current && defaultFilterReqDataRef.current?.current)
89
99
  reqData.current = defaultFilterReqDataRef.current.current
@@ -101,17 +111,25 @@ function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunc
101
111
  joins: formJoinsRef.current,
102
112
  project: JSON.stringify(dataProjectRef.current),
103
113
  ...restReqData,
104
- // match: JSON.stringify({"Data.phaseId": 16673})
105
114
  })
106
115
  reportDataApiCancelFuncRef.current = cancel
107
116
 
108
117
  request
109
118
  .then((res) => {
110
- if (res.status === 200) setDataList({ data: res.data.data, total: res.data.totalRecords })
119
+ if (res.status === 200) {
120
+ setDataList({ data: res.data.data, total: res.data.totalRecords })
121
+ }
122
+ })
123
+ .catch((err) => {
124
+ if (err?.response?.status === 500) {
125
+ destroy()
126
+ warning({ message: 'There has been an error fetching the data!' })
127
+ }
111
128
  })
112
129
  .finally(() => {
113
- setLoadings({ initial: false, data: false })
130
+ apiCallCounterRef.current -= 1
114
131
  reportDataApiCancelFuncRef.current = undefined
132
+ if (apiCallCounterRef.current === 0) setDataLoading(false)
115
133
  })
116
134
  },
117
135
  [formId],
@@ -122,20 +140,17 @@ function FormDataListComponent({ formId, companyKey, baseServerUrl, onCustomFunc
122
140
  layoutsConfigs={listLayoutConfig}
123
141
  dataList={dataList}
124
142
  updateDataList={(reqData) => {
125
- if (reportDataApiCancelFuncRef.current)
126
- // prev request
127
- reportDataApiCancelFuncRef.current()
143
+ if (reportDataApiCancelFuncRef.current) reportDataApiCancelFuncRef.current()
128
144
 
129
145
  fetchFormDataList(reqData)
130
146
  }}
131
- parentLoadings={loadings}
147
+ parentLoadings={{ data: dataLoading, initial: isConfigLoading || !cachedConfig }}
132
148
  loadingBlock={<FormDataListSkeleton_Table />}
133
- setParentLoading={(bool) => setLoadings((c) => ({ ...c, data: bool as boolean }))}
149
+ setParentLoading={(bool) => setDataLoading(bool)}
134
150
  headerLayoutContext={headerLayoutContext}
135
151
  />
136
152
  )
137
153
  }
138
- export { FormDataListComponent }
139
154
 
140
155
  type IFormDataListComponent = {
141
156
  baseServerUrl?: string
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react'
2
2
  import FormDataListHeaderComponent from './table-header'
3
3
  import useDebounced from '../../common/custom-hooks/use-debounce.hook'
4
- import { Table } from 'antd'
4
+ import { Spin, 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
7
  import { constructDynamicFormHref, renderTableColumns, revertProjectionKey } from '../../../functions/forms'
@@ -100,26 +100,25 @@ export default function FormDataListTableComponent({
100
100
  [tableColumns, currentBreakpoint],
101
101
  )
102
102
 
103
+ // if (loading) return
103
104
  if (loading && loadingBlock) return loadingBlock
104
105
 
105
106
  return (
106
107
  <>
107
- {!parentLoadings.initial && (
108
- <FormDataListHeaderComponent
109
- layoutConfig={layoutsConfigs?.dataListConfig.header}
110
- updateDynamicFilter={(match) => {
111
- setParentLoading(true)
112
- setFilterReqData((c) => ({ ...c, match, current: 1 }))
113
- }}
114
- titleComponent={
115
- <>
116
- <span className="pr-1">{otherConfigs?.title}</span>
117
- {otherConfigs?.showCount && <span>({dataList.total})</span>}
118
- </>
119
- }
120
- headerLayoutContext={headerLayoutContext}
121
- />
122
- )}
108
+ <FormDataListHeaderComponent
109
+ layoutConfig={layoutsConfigs?.dataListConfig.header}
110
+ updateDynamicFilter={(match) => {
111
+ setParentLoading(true)
112
+ setFilterReqData((c) => ({ ...c, match, current: 1 }))
113
+ }}
114
+ titleComponent={
115
+ <>
116
+ <span className="pr-1">{otherConfigs?.title}</span>
117
+ {otherConfigs?.showCount && <span>({parentLoadings.data ? <Spin size="small" /> : dataList.total})</span>}
118
+ </>
119
+ }
120
+ headerLayoutContext={headerLayoutContext}
121
+ />
123
122
  {(!otherConfigs || otherConfigs.listType === FormDataListViewTypeEnum.Table) && (
124
123
  <Table<IFormDataListData>
125
124
  dataSource={dataList.data}
@@ -1,6 +1,6 @@
1
- import { Form } from 'antd'
1
+ import { Form, Spin } from 'antd'
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
- import { IDndLayoutStructure_Responsive, IFormSchema } from '../../../types'
3
+ import { IDndLayoutStructure_Responsive } 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'
@@ -10,12 +10,7 @@ import client from '../../../api/client'
10
10
  import { useBreadcrumb } from '../../common/custom-hooks/use-breadcrumb.hook'
11
11
  import NotFound from '../../common/not-found'
12
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'
13
+ import { PageViewTypEnum, DeviceBreakpointEnum, FormPreservedItemKeys, LOCAL_STORAGE_KEYS_ENUM } from '../../../enums'
19
14
  import {
20
15
  fromMongoDbExtendedJSON,
21
16
  getPickerFieldsWithOriginalTz,
@@ -23,6 +18,8 @@ import {
23
18
  isValidMongoDbId,
24
19
  queryParamsToObject,
25
20
  } from '../../../functions/forms'
21
+ import { UserAuth } from '../../../api/user'
22
+ import { useCacheFormLayoutConfig } from '../../common/custom-hooks/use-cache-form-layout-config.hook'
26
23
 
27
24
  export default function FormDataDetailsComponent({
28
25
  isPublic,
@@ -38,14 +35,15 @@ export default function FormDataDetailsComponent({
38
35
  const { push } = useBreadcrumb()
39
36
  const location = useLocation()
40
37
  const [formDataRef] = Form.useForm()
41
- const [loadings, setLoadings] = useState({ layout: true, data: true })
42
- const [layoutConfig, setLayoutConfig] = useState<IDndLayoutStructure_Responsive>({ elements: {}, layouts: {} })
38
+ const [loading, setLoading] = useState(true)
43
39
  const [isNotFound, setIsNotFound] = useState(false)
44
40
  const originalTzFieldsRef = useRef<string[]>([])
45
41
  useManyToManyConnector(formDataRef)
46
42
 
47
43
  const currentBreakpoint = useGetCurrentBreakpoint()
48
44
 
45
+ const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(formId)
46
+
49
47
  useEffect(() => {
50
48
  // for public forms, setting the details form into localStorage, so that buttons work correctly
51
49
  if (isPublic)
@@ -61,15 +59,10 @@ export default function FormDataDetailsComponent({
61
59
  push({
62
60
  label: isNewFormDataPage(formDataId) ? splittedFormName.toLowerCase() : splittedFormName,
63
61
  href: location.pathname,
64
- type: isNewFormDataPage(formDataId) ? BreadcrumbTypeEnum.New : BreadcrumbTypeEnum.Details,
62
+ type: isNewFormDataPage(formDataId) ? PageViewTypEnum.New : PageViewTypEnum.Details,
65
63
  })
66
64
  }, [location.pathname, formName, formDataId])
67
65
 
68
- const layout = useMemo(
69
- () => layoutConfig.layouts[currentBreakpoint] ?? layoutConfig.layouts[DeviceBreakpointEnum.Default] ?? [],
70
- [layoutConfig.layouts, currentBreakpoint],
71
- )
72
-
73
66
  // Apply initialValues from parent
74
67
  useEffect(() => {
75
68
  if (initialValues && isNewFormDataPage(formDataId)) {
@@ -96,7 +89,7 @@ export default function FormDataDetailsComponent({
96
89
 
97
90
  const fetchFormData = useCallback(
98
91
  (dFormId?: number) => {
99
- if (isNewFormDataPage(formDataId) || !isValidMongoDbId(formDataId)) setLoadings((c) => ({ ...c, data: false }))
92
+ if (isNewFormDataPage(formDataId) || !isValidMongoDbId(formDataId)) setLoading(false)
100
93
  else {
101
94
  if (!dFormId) {
102
95
  console.error('Form ID is required to fetch form data')
@@ -119,69 +112,73 @@ export default function FormDataDetailsComponent({
119
112
  }
120
113
  } else setIsNotFound(true)
121
114
  })
122
- .finally(() => setLoadings((c) => ({ ...c, data: false })))
115
+ .finally(() => setLoading(false))
123
116
  }
124
117
  },
125
118
  [formDataId, formDataRef],
126
119
  )
127
120
 
128
121
  useEffect(() => {
129
- if (formId || formKey) {
130
- const endpoint = isPublic ? `/api/site/${formKey}` : `/api/form/${formId}`
131
- client
132
- .get(endpoint)
133
- .then((res) => {
134
- if (res.status === 200) {
135
- const parsedData: IFormSchema | null = JSON.parse(res.data.data)
136
- if (parsedData) {
137
- console.log('FORM LAYOUT FETCH (PARSED)', parsedData)
138
- const { layouts: rawLayouts, elements } = parsedData.detailsConfig
139
- originalTzFieldsRef.current = getPickerFieldsWithOriginalTz(elements)
140
-
141
- if (isPublic) {
142
- if (parsedData.generateConfig?.submissionPdf?.enabled)
143
- formDataRef.setFieldValue(
144
- FormPreservedItemKeys.SubmissionPdfConfig,
145
- parsedData.generateConfig.submissionPdf,
146
- )
147
- setLoadings((c) => ({ ...c, data: false }))
148
- } else fetchFormData(formId)
149
-
150
- setLayoutConfig({ elements, layouts: rawLayouts })
151
- }
152
- }
153
- })
154
- .finally(() => setLoadings((c) => ({ ...c, layout: false })))
155
- }
156
- }, [formId, formKey, isPublic, formDataRef, fetchFormData])
122
+ if (!cachedConfig?.detailsConfig) return
123
+
124
+ const { elements } = cachedConfig.detailsConfig as IDndLayoutStructure_Responsive
125
+ originalTzFieldsRef.current = getPickerFieldsWithOriginalTz(elements)
126
+
127
+ if (isPublic) {
128
+ if (cachedConfig.generateConfig?.submissionPdf?.enabled)
129
+ formDataRef.setFieldValue(FormPreservedItemKeys.SubmissionPdfConfig, cachedConfig.generateConfig.submissionPdf)
130
+ setLoading(false)
131
+ } else fetchFormData(formId)
132
+ }, [cachedConfig, formId, isPublic, formDataRef, fetchFormData])
133
+
134
+ const layout = useMemo(() => {
135
+ if (!cachedConfig?.detailsConfig?.layouts) return []
136
+
137
+ const deviceLayout =
138
+ cachedConfig.detailsConfig.layouts[currentBreakpoint] ??
139
+ cachedConfig.detailsConfig.layouts[DeviceBreakpointEnum.Default]
140
+
141
+ if (!deviceLayout) return []
142
+
143
+ return deviceLayout
144
+ }, [cachedConfig?.detailsConfig, currentBreakpoint])
157
145
 
158
146
  const formContext = useMemo(
159
- () => ({ detailPageFormId: formId, formId, formKey, formDataId, formRef: formDataRef, companyKey }),
147
+ () => ({
148
+ detailPageFormId: formId,
149
+ formId,
150
+ formKey,
151
+ formDataId,
152
+ formRef: formDataRef,
153
+ companyKey: UserAuth.getCompanyKey(),
154
+ }),
160
155
  [formDataId, formDataRef, formId, formKey, companyKey],
161
156
  )
162
157
 
163
- if (loadings.layout || loadings.data) return <FormDataListSkeleton_Details />
158
+ if (isConfigLoading || !cachedConfig) return <FormDataListSkeleton_Details />
164
159
 
165
160
  if (isNotFound) return <NotFound />
166
161
 
167
162
  return (
168
- <Form layout="vertical" name="dynamic_form_data_form" form={formDataRef}>
169
- {layout.map((row, rowIdx) => (
170
- <LayoutRendererRow
171
- key={rowIdx}
172
- rowData={row}
173
- formContext={formContext}
174
- elements={layoutConfig.elements}
175
- renderButton={(btnProps, conditions) => (
176
- <DynamicFormButtonRender
177
- displayStateProps={{ btnProps, conditions, defaultDisabled: true }}
178
- formContext={formContext}
179
- onCustomFunctionCall={onCustomFunctionCall}
180
- />
181
- )}
182
- />
183
- ))}
184
- </Form>
163
+ <Spin spinning={loading}>
164
+ <Form layout="vertical" name="dynamic_form_data_form" form={formDataRef}>
165
+ {layout.map((row, rowIdx) => (
166
+ <LayoutRendererRow
167
+ key={rowIdx}
168
+ rowData={row}
169
+ formContext={formContext}
170
+ elements={cachedConfig.detailsConfig.elements}
171
+ renderButton={(btnProps, conditions) => (
172
+ <DynamicFormButtonRender
173
+ displayStateProps={{ btnProps, conditions, defaultDisabled: true }}
174
+ formContext={formContext}
175
+ onCustomFunctionCall={onCustomFunctionCall}
176
+ />
177
+ )}
178
+ />
179
+ ))}
180
+ </Form>
181
+ </Spin>
185
182
  )
186
183
  }
187
184
 
File without changes
@@ -3,7 +3,7 @@ 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'
6
+ import { PageViewTypEnum } from '../../../../enums'
7
7
  import { IBreadcrumbElementProps } from '../../../../types'
8
8
 
9
9
  export default function LayoutRenderer_Breadcrumb({ elementProps }: { elementProps: IBreadcrumbElementProps }) {
@@ -23,11 +23,11 @@ export default function LayoutRenderer_Breadcrumb({ elementProps }: { elementPro
23
23
  }}
24
24
  >
25
25
  <span className="font-normal italic">
26
- {bc.type === BreadcrumbTypeEnum.New ? `${elementProps.newText} ` : ''}
26
+ {bc.type === PageViewTypEnum.New ? `${elementProps.newText} ` : ''}
27
27
  {bc.label}
28
- {bc.type === BreadcrumbTypeEnum.Details
28
+ {bc.type === PageViewTypEnum.Details
29
29
  ? ` ${elementProps.detailText}`
30
- : bc.type === BreadcrumbTypeEnum.List
30
+ : bc.type === PageViewTypEnum.List
31
31
  ? ` ${elementProps.listText}`
32
32
  : ''}
33
33
  </span>
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
- import { BreadcrumbTypeEnum, FieldElementOptionSourceEnum, FilterConfigTypeEnum } from '../../../../enums'
2
+ import { PageViewTypEnum, 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'
@@ -275,7 +275,7 @@ export default function LayoutRenderer_FieldsWithOptions({
275
275
  const formInfo = getFormById(props.optionSource.formId)
276
276
  if (!formInfo) return
277
277
 
278
- push({ label: formInfo.name, href: location.pathname, type: BreadcrumbTypeEnum.Details })
278
+ push({ label: formInfo.name, href: location.pathname, type: PageViewTypEnum.Details })
279
279
  navigate(`${constructDynamicFormHref(formInfo.name)}/${selectedValue}`)
280
280
  }}
281
281
  >
@@ -6,13 +6,13 @@ import { getIdEqualsQuery, getProjectionKey, isNewFormDataPage, mergeJoins } fro
6
6
  import { cancelableClient } from '../../../../api/client'
7
7
  import { IFormContext } from '../1-row'
8
8
  import { FilterConfigTypeEnum, FormRelationshipEnum } from '../../../../enums'
9
+ import { useCacheFormLayoutConfig } from '../../../common/custom-hooks/use-cache-form-layout-config.hook'
9
10
  import {
10
11
  IFormDataListData,
11
12
  IFormDataLoadElementProps,
12
13
  IFormJoin,
13
14
  IFormRelationshipConfig,
14
15
  IFormRelationshipConfig_OneToMany,
15
- IFormSchema,
16
16
  } from '../../../../types'
17
17
 
18
18
  export default function LayoutRenderer_LoadFormData({ formContext, elementProps }: ILayoutRenderer_LoadFormData) {
@@ -22,9 +22,11 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
22
22
  formName: '',
23
23
  parentFormJoins: [],
24
24
  })
25
- const [loadings, setLoadings] = useState({ initial: true, data: true })
25
+ const [loading, setLoading] = useState(true)
26
26
  const [listLayoutConfig, setListLayoutConfig] = useState<IDataListLayoutConfig>()
27
27
 
28
+ const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(baseFormId)
29
+
28
30
  const [dataList, setDataList] = useState<{ data: IFormDataListData[]; total: number }>({ data: [], total: 0 })
29
31
  const dataProjectRef = useRef<{ [key: string]: string }>({})
30
32
  const reportDataApiCancelFuncRef = useRef<() => void | undefined>(undefined)
@@ -38,7 +40,7 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
38
40
  : '',
39
41
  ) => {
40
42
  if (!baseFormId) return
41
- setLoadings((c) => ({ ...c, data: true }))
43
+ setLoading(true)
42
44
 
43
45
  const matchFilters: { [key: string]: any }[] = [getIdEqualsQuery(formId, formDataId)]
44
46
  if (headerAppliedFilters) matchFilters.push(JSON.parse(headerAppliedFilters))
@@ -61,72 +63,63 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
61
63
  setDataList({ data: res.data.data, total: res.data.totalRecords })
62
64
  }
63
65
  })
64
- .finally(() => setLoadings({ initial: false, data: false }))
66
+ .finally(() => setLoading(false))
65
67
  },
66
68
  [joins, baseFormId, initialFilter],
67
69
  )
68
70
 
69
71
  useEffect(() => {
70
- if (!baseFormId || isNewFormDataPage(formDataId)) return
71
-
72
- // fetching display config (base form can be different than display config form)
73
- const { request, cancel } = cancelableClient.get(`/api/form/${baseFormId}`)
74
- request.then((res) => {
75
- if (res.status === 200) {
76
- const parsedData: IFormSchema | null = JSON.parse(res.data.data)
77
- if (!parsedData) return
78
- const { relationships = [] } = parsedData
79
-
80
- const relationship: IFormRelationshipConfig | undefined = relationships.find((r: IFormRelationshipConfig) =>
81
- r.displayConfigs?.some((dc) => dc.id === displayConfigId),
82
- )
83
- const displayConfig = relationship?.displayConfigs?.find((dc) => dc.id === displayConfigId)
84
-
85
- if (!displayConfig || displayConfig.isRenderChildForm) return
86
-
87
- if (relationship!.type === FormRelationshipEnum.OneToMany && relationship?.m2mTargetFormFK)
88
- m2mForeignKeysRef.current = {
89
- currentForm: relationship!.foreignKey,
90
- otherForm: (relationship as IFormRelationshipConfig_OneToMany)!.m2mTargetFormFK!,
91
- }
72
+ if (isNewFormDataPage(formDataId) || !cachedConfig) return
73
+
74
+ const { relationships = [] } = cachedConfig
92
75
 
93
- lastChildInfo.current = { formName: res.data.name, parentFormJoins: joins }
94
-
95
- if (displayConfig.config) {
96
- formJoinsRef.current = mergeJoins([
97
- ...displayConfig.config.columns.map((c) => c.joins),
98
- ...Object.values(displayConfig.config.header.elements).reduce((c: IFormJoin[], el) => {
99
- if (el.props && 'filter' in el.props && Array.isArray(el.props.filter?.joins)) {
100
- return [...c, el.props.filter.joins]
101
- }
102
- return c
103
- }, []),
104
- ])
105
- if (Array.isArray(displayConfig.config.columns))
106
- dataProjectRef.current = displayConfig.config.columns.reduce((curr, c) => {
107
- if (!c.key) return curr
108
-
109
- return { ...curr, [getProjectionKey(c.key)]: `$${c.key}` }
110
- }, {})
111
-
112
- setListLayoutConfig({
113
- configForFormId: baseFormId,
114
- dataListConfig: {
115
- ...displayConfig.config,
116
- columns: displayConfig.config.columns.map((c) => (c.key ? { ...c, key: getProjectionKey(c.key) } : c)),
117
- },
118
- })
119
-
120
- fetchDataList()
121
- }
76
+ const relationship: IFormRelationshipConfig | undefined = relationships.find((r: IFormRelationshipConfig) =>
77
+ r.displayConfigs?.some((dc) => dc.id === displayConfigId),
78
+ )
79
+ const displayConfig = relationship?.displayConfigs?.find((dc) => dc.id === displayConfigId)
80
+
81
+ if (!displayConfig || displayConfig.isRenderChildForm) return
82
+
83
+ if (relationship!.type === FormRelationshipEnum.OneToMany && relationship?.m2mTargetFormFK)
84
+ m2mForeignKeysRef.current = {
85
+ currentForm: relationship!.foreignKey,
86
+ otherForm: (relationship as IFormRelationshipConfig_OneToMany)!.m2mTargetFormFK!,
122
87
  }
123
- })
88
+
89
+ lastChildInfo.current = { formName: cachedConfig.name, parentFormJoins: joins }
90
+
91
+ if (displayConfig.config) {
92
+ formJoinsRef.current = mergeJoins([
93
+ ...displayConfig.config.columns.map((c) => c.joins),
94
+ ...Object.values(displayConfig.config.header.elements).reduce((c: IFormJoin[], el) => {
95
+ if (el.props && 'filter' in el.props && Array.isArray(el.props.filter?.joins)) {
96
+ return [...c, el.props.filter.joins]
97
+ }
98
+ return c
99
+ }, []),
100
+ ])
101
+ if (Array.isArray(displayConfig.config.columns))
102
+ dataProjectRef.current = displayConfig.config.columns.reduce((curr, c) => {
103
+ if (!c.key) return curr
104
+
105
+ return { ...curr, [getProjectionKey(c.key)]: `$${c.key}` }
106
+ }, {})
107
+
108
+ setListLayoutConfig({
109
+ configForFormId: baseFormId,
110
+ dataListConfig: {
111
+ ...displayConfig.config,
112
+ columns: displayConfig.config.columns.map((c) => (c.key ? { ...c, key: getProjectionKey(c.key) } : c)),
113
+ },
114
+ })
115
+
116
+ fetchDataList()
117
+ }
124
118
 
125
119
  return () => {
126
- cancel()
127
120
  reportDataApiCancelFuncRef.current?.()
128
121
  }
129
- }, [baseFormId, displayConfigId, formDataId, fetchDataList])
122
+ }, [cachedConfig, displayConfigId, formDataId, fetchDataList])
130
123
 
131
124
  const dataListHeaderContext = {
132
125
  detailPageFormId: formId,
@@ -156,9 +149,9 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
156
149
  updateDataList={(filter) => {
157
150
  fetchDataList(filter.match)
158
151
  }}
159
- parentLoadings={loadings}
152
+ parentLoadings={{ data: loading, initial: isConfigLoading }}
160
153
  loadingBlock={<FormDataListSkeleton_Table small />}
161
- setParentLoading={(bool) => setLoadings((c) => ({ ...c, data: bool }))}
154
+ setParentLoading={(bool) => setLoading(bool)}
162
155
  headerLayoutContext={dataListHeaderContext}
163
156
  />
164
157
  )
package/src/constants.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { CountryEnum, CSSLayoutType, DeviceBreakpointEnum } from './enums'
1
+ import { QueryClient } from '@tanstack/react-query'
2
+ import { CountryEnum, CSSLayoutType, DeviceBreakpointEnum, FormDataListViewTypeEnum } from './enums'
3
+ import { IFormSchema } from './types'
2
4
 
3
5
  export const CLIENT_ID = 'admin'
4
6
  export const CLIENT_SECRET = '3e9e418c-8bd7-4128-90c2-8db3e586a283'
@@ -170,3 +172,22 @@ export const BSON_DATA_IDENTIFIER_PREFIXES = {
170
172
  Date: 'Date__',
171
173
  Number: 'Number__',
172
174
  }
175
+ export const DEFAULT_FORM_SCHEMA_DATA: IFormSchema = {
176
+ detailsConfig: { elements: {}, layouts: { [DeviceBreakpointEnum.Default]: [] } },
177
+ dataListConfig: {
178
+ listType: FormDataListViewTypeEnum.Table,
179
+ columns: [],
180
+ header: { elements: {}, layouts: { [DeviceBreakpointEnum.Default]: [] } },
181
+ },
182
+ generateConfig: { submissionPdf: { enabled: false } },
183
+ relationships: [],
184
+ }
185
+ export const REACT_QUERY_CLIENT = new QueryClient({
186
+ defaultOptions: {
187
+ queries: {
188
+ staleTime: 10 * 60 * 60 * 1000, // 10 hours
189
+ // refetchOnWindowFocus: true,
190
+ refetchOnMount: 'always',
191
+ },
192
+ },
193
+ })
@@ -198,7 +198,7 @@ export enum ReadFieldDataValueTypeEnum {
198
198
  Default = 'Default',
199
199
  Evaluated = 'Evaluated',
200
200
  }
201
- export enum BreadcrumbTypeEnum {
201
+ export enum PageViewTypEnum {
202
202
  Details = 1,
203
203
  List,
204
204
  New,
@@ -9,7 +9,8 @@ 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
+ import { PageViewTypEnum } from '../../../../enums'
13
+ import { REACT_QUERY_CLIENT } from '../../../../constants'
13
14
 
14
15
  const { Header, Content, Sider } = Layout
15
16
 
@@ -26,6 +27,7 @@ export const useMenuItems = () => {
26
27
  icon: FaIcons.FaKey,
27
28
  onClick: async () => {
28
29
  await cookieHandler.empty()
30
+ REACT_QUERY_CLIENT.clear()
29
31
  localStorage.clear()
30
32
  navigate('/login')
31
33
  },
@@ -492,7 +494,7 @@ export const layoutTemplates = [
492
494
  onClick={() => {
493
495
  reset()
494
496
  const splittedFormName = form.name.split(' ')?.[0] ?? ''
495
- push({ label: splittedFormName, href, type: BreadcrumbTypeEnum.List })
497
+ push({ label: splittedFormName, href, type: PageViewTypEnum.List })
496
498
 
497
499
  if (isParentMenu) toggleMenu(form.id)
498
500
  }}
@@ -10,6 +10,16 @@ import { IFormRelationshipConfig } from './relationship'
10
10
  import { IButtonElementProps } from './layout-elements'
11
11
  import { IDndLayoutStructure_Responsive } from '..'
12
12
 
13
+ export interface IFormConfigDetails {
14
+ description: string
15
+ id: number
16
+ isUser: boolean
17
+ keepVersionHistory: boolean
18
+ key: string
19
+ name: string
20
+ order: number
21
+ version: number
22
+ }
13
23
  export interface IFormSchema {
14
24
  isRelationshipForm?: boolean
15
25
  detailsConfig: IDndLayoutStructure_Responsive