form-craft-package 1.10.12-dev.0 → 1.10.13-dev.1

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 (70) hide show
  1. package/.env.development +2 -1
  2. package/package.json +1 -1
  3. package/src/api/client.ts +47 -5
  4. package/src/api/user.ts +14 -25
  5. package/src/components/auth-layouts/index.tsx +439 -0
  6. package/src/components/auth-layouts/template-hooks.tsx +124 -0
  7. package/src/components/common/custom-hooks/index.ts +1 -0
  8. package/src/components/common/custom-hooks/use-cache-form-layout-config.hook.ts +2 -2
  9. package/src/components/common/custom-hooks/use-duplicate-on-blur.hook.ts +185 -185
  10. package/src/components/common/custom-hooks/use-find-dynamic-form.hook.ts +6 -7
  11. package/src/components/common/custom-hooks/use-node-condition.hook/use-disabled-elements.hook.ts +30 -3
  12. package/src/components/common/custom-hooks/use-node-condition.hook/visibility-utils.ts +18 -18
  13. package/src/components/common/custom-hooks/use-preserved-form-items.hook.ts +13 -5
  14. package/src/components/common/custom-hooks/use-user-permission.hook.ts +55 -0
  15. package/src/components/common/duplicate-entry-checker/duplicate-warning.modal.tsx +93 -93
  16. package/src/components/common/duplicate-entry-checker/index.tsx +54 -54
  17. package/src/components/common/results/403.tsx +22 -0
  18. package/src/components/common/{not-found.tsx → results/not-found.tsx} +1 -1
  19. package/src/components/companies/1-authenticated/index.tsx +13 -11
  20. package/src/components/companies/index.tsx +0 -1
  21. package/src/components/form/1-list/table.tsx +1 -1
  22. package/src/components/form/2-details/index.tsx +102 -22
  23. package/src/components/form/layout-renderer/1-row/index.tsx +5 -7
  24. package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +166 -18
  25. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-action-permissions.hook.ts +82 -0
  26. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-navigate.hook.tsx +2 -2
  27. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-create-data.hook.ts +28 -97
  28. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-save-user-account-action.hook/helper.ts +210 -0
  29. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-save-user-account-action.hook/index.tsx +263 -0
  30. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-send-notification.hook.ts +96 -96
  31. package/src/components/form/layout-renderer/3-element/10-currency.tsx +28 -6
  32. package/src/components/form/layout-renderer/3-element/11-breadcrumb/index.tsx +52 -30
  33. package/src/components/form/layout-renderer/3-element/12-picker-field.tsx +36 -11
  34. package/src/components/form/layout-renderer/3-element/13-language-selector/index.tsx +33 -10
  35. package/src/components/form/layout-renderer/3-element/14-auto-complete.tsx +127 -108
  36. package/src/components/form/layout-renderer/3-element/16-user-role.tsx +30 -8
  37. package/src/components/form/layout-renderer/3-element/2-field-element.tsx +75 -43
  38. package/src/components/form/layout-renderer/3-element/3-read-field-data.tsx +17 -2
  39. package/src/components/form/layout-renderer/3-element/4-rich-text-editor.tsx +16 -2
  40. package/src/components/form/layout-renderer/3-element/6-signature.tsx +187 -174
  41. package/src/components/form/layout-renderer/3-element/7-file-upload.tsx +37 -6
  42. package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +42 -8
  43. package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +209 -209
  44. package/src/components/form/layout-renderer/3-element/index.tsx +148 -22
  45. package/src/components/index.tsx +1 -0
  46. package/src/components/modals/change-password.modal.tsx +250 -0
  47. package/src/components/modals/save-user-account.modal.tsx +81 -0
  48. package/src/{constants.ts → constants/index.ts} +17 -20
  49. package/src/enums/form.enum.ts +17 -7
  50. package/src/enums/index.ts +20 -4
  51. package/src/functions/companies/index.tsx +3 -0
  52. package/src/functions/companies/translation-key-builder/breadcrumb-translations.ts +5 -0
  53. package/src/functions/companies/translation-key-builder/password-translations.ts +12 -0
  54. package/src/functions/companies/translation-key-builder/role-permission-translations.ts +12 -0
  55. package/src/functions/cookie-handler.ts +3 -2
  56. package/src/functions/forms/conditional-rule.utils.ts +264 -164
  57. package/src/functions/forms/conditional-text.ts +48 -0
  58. package/src/functions/forms/create-form-rules.ts +77 -16
  59. package/src/functions/forms/index.ts +199 -205
  60. package/src/types/companies/index.ts +13 -8
  61. package/src/types/forms/data-list/index.ts +2 -0
  62. package/src/types/forms/index.ts +35 -34
  63. package/src/types/forms/layout-elements/button.ts +10 -1
  64. package/src/types/forms/layout-elements/conditions.ts +20 -0
  65. package/src/types/forms/layout-elements/index.ts +3 -1
  66. package/src/types/notifications/index.ts +6 -6
  67. package/src/components/companies/1-authenticated/change-password.tsx +0 -110
  68. package/src/types/companies/roles.ts +0 -14
  69. package/src/types/companies/site-layout/authenticated/index.tsx +0 -736
  70. package/src/types/companies/site-layout/unauthenticated/index.tsx +0 -26
package/.env.development CHANGED
@@ -1,4 +1,5 @@
1
- VITE_API_BASE_URL=https://formcraftmaster-backend-dev-f8egf2fcfhhahuhr.centralus-01.azurewebsites.net
1
+ # VITE_API_BASE_URL=https://formcraftmaster-backend-dev-f8egf2fcfhhahuhr.centralus-01.azurewebsites.net
2
+ VITE_API_BASE_URL=http://localhost:5029
2
3
  VITE_LOG_LEVEL=debug
3
4
  VITE_COOKIE_DOMAIN=localhost
4
5
  VITE_ENV=development
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.10.12-dev.0",
3
+ "version": "1.10.13-dev.1",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
package/src/api/client.ts CHANGED
@@ -10,7 +10,7 @@ import axios, {
10
10
  InternalAxiosRequestConfig,
11
11
  } from 'axios'
12
12
 
13
- import { PageViewTypEnum, LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS } from '../enums'
13
+ import { PageViewTypEnum, LOCAL_STORAGE_KEYS_ENUM, SHARED_COOKIE_KEYS, SystemRolePermissionEnum } from '../enums'
14
14
  import { IDynamicForm } from '../types'
15
15
  import { constructDynamicFormHref, cookieHandler, fetchDynamicForms } from '../functions'
16
16
  import { CLIENT_ID, CLIENT_SECRET } from '../constants'
@@ -27,12 +27,17 @@ apiClient.interceptors.request.use((config) => {
27
27
  return config
28
28
  })
29
29
 
30
- const authResHandler = (resData: {
30
+ type AuthResponseData = {
31
31
  access_token: string
32
32
  refresh_token: string
33
33
  companyKey: string
34
34
  expires_in: number
35
- }) => {
35
+ id?: string
36
+ roles?: string
37
+ email: string
38
+ }
39
+
40
+ const authResHandler = (resData: AuthResponseData) => {
36
41
  const expiry = new Date(Date.now() + resData.expires_in * 1000)
37
42
  Cookies.set(SHARED_COOKIE_KEYS.AccessToken, resData.access_token, {
38
43
  expires: expiry,
@@ -43,6 +48,17 @@ const authResHandler = (resData: {
43
48
  path: '/',
44
49
  })
45
50
  Cookies.set(SHARED_COOKIE_KEYS.CompanyKey, resData.companyKey, { expires: expiry, path: '/' })
51
+ Cookies.set(SHARED_COOKIE_KEYS.UserEmail, resData.email, { expires: expiry, path: '/' })
52
+
53
+ let permissionNames: string[] = resData.roles ? JSON.parse(resData.roles) ?? [] : []
54
+ permissionNames = Array.isArray(permissionNames) ? permissionNames.filter(Boolean) : []
55
+ const permissions = permissionNames
56
+ .map((name) => SystemRolePermissionEnum[name as keyof typeof SystemRolePermissionEnum])
57
+ .filter((permission): permission is SystemRolePermissionEnum => typeof permission === 'number')
58
+
59
+ if (permissions.length)
60
+ Cookies.set(SHARED_COOKIE_KEYS.UserPermissions, JSON.stringify(permissions), { expires: expiry, path: '/' })
61
+ else Cookies.remove(SHARED_COOKIE_KEYS.UserPermissions)
46
62
  }
47
63
 
48
64
  let isRefreshing = false
@@ -179,14 +195,40 @@ export const auth = async (
179
195
  Cookies.set(SHARED_COOKIE_KEYS.Authorized, 'true')
180
196
  forms = await fetchDynamicForms()
181
197
 
198
+ let userRoleIds: string[] = []
199
+ const userForm = forms.find((form) => form.isUser)
200
+ if (userForm?.id && loginAuthRes.data.id) {
201
+ try {
202
+ const userRolesRes = await apiClient.post(`/api/report/data/${userForm.id}`, {
203
+ joins: [],
204
+ match: JSON.stringify({ DeletedDate: null, 'Data.loginUser_id': loginAuthRes.data.id }),
205
+ project: JSON.stringify({ Data_roles: '$Data.loginUser_roles' }),
206
+ skip: 0,
207
+ limit: 1,
208
+ })
209
+ const fetchedRoles = userRolesRes?.data?.data?.[0]?.Data_roles
210
+ if (Array.isArray(fetchedRoles))
211
+ userRoleIds = fetchedRoles.filter((role): role is string => typeof role === 'string' && role.length > 0)
212
+ } catch {}
213
+ }
214
+
215
+ if (userRoleIds.length)
216
+ Cookies.set(SHARED_COOKIE_KEYS.UserRoleNames, JSON.stringify(userRoleIds), {
217
+ expires: new Date(Date.now() + loginAuthRes.data.expires_in * 1000),
218
+ path: '/',
219
+ })
220
+ else Cookies.remove(SHARED_COOKIE_KEYS.UserRoleNames)
221
+
182
222
  breadcrumbStore.reset()
183
223
  if (forms.length) {
184
224
  const firstForm = forms[0]
185
225
  const [splittedFormName] = firstForm.name.split(' ')
186
226
  breadcrumbStore.push({
187
227
  label: splittedFormName,
188
- href: constructDynamicFormHref(firstForm.name),
228
+ href: constructDynamicFormHref(firstForm),
189
229
  type: PageViewTypEnum.List,
230
+ formId: firstForm.id,
231
+ formName: firstForm.name,
190
232
  })
191
233
  }
192
234
 
@@ -197,7 +239,7 @@ export const auth = async (
197
239
 
198
240
  return {
199
241
  authRes: loginAuthRes,
200
- initialPath: forms.length > 0 ? constructDynamicFormHref(forms[0].name) : '/accounts',
242
+ initialPath: forms.length > 0 ? constructDynamicFormHref(forms[0]) : '/accounts',
201
243
  }
202
244
  } catch (error) {
203
245
  return { authRes: error, initialPath: '' }
package/src/api/user.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import { SHARED_COOKIE_KEYS } from '../enums'
2
2
  import { cookieHandler } from '../functions/cookie-handler'
3
- import crypto from 'crypto-js'
4
- import { CLIENT_SECRET } from '../constants'
3
+ // import crypto from 'crypto-js'
4
+ // import { CLIENT_SECRET } from '../constants'
5
5
 
6
- const endSession = () => {
7
- cookieHandler.empty()
8
- localStorage.clear()
9
- return
10
- }
11
- const decrypt = (toDecrypt: SHARED_COOKIE_KEYS) => {
12
- const cookieValue = cookieHandler.get(toDecrypt)
13
- if (!cookieValue) return ''
14
- const bytes = crypto.AES.decrypt(cookieValue, CLIENT_SECRET)
15
- return JSON.parse(bytes.toString(crypto.enc.Utf8))
16
- }
6
+ // const endSession = () => {
7
+ // cookieHandler.empty()
8
+ // localStorage.clear()
9
+ // return
10
+ // }
11
+ // const decrypt = (toDecrypt: SHARED_COOKIE_KEYS) => {
12
+ // const cookieValue = cookieHandler.get(toDecrypt)
13
+ // if (!cookieValue) return ''
14
+ // const bytes = crypto.AES.decrypt(cookieValue, CLIENT_SECRET)
15
+ // return JSON.parse(bytes.toString(crypto.enc.Utf8))
16
+ // }
17
17
 
18
18
  export const UserAuth = {
19
19
  hasRefreshToken: () => {
@@ -29,18 +29,7 @@ export const UserAuth = {
29
29
  return cookieHandler.get(SHARED_COOKIE_KEYS.CompanyKey)
30
30
  },
31
31
  getUserEmail: () => {
32
- try {
33
- return decrypt(SHARED_COOKIE_KEYS.UserEmail) ? decrypt(SHARED_COOKIE_KEYS.UserEmail) : ''
34
- } catch {
35
- endSession()
36
- }
37
- },
38
- getUserFullName: () => {
39
- try {
40
- return decrypt(SHARED_COOKIE_KEYS.FullName) ? decrypt(SHARED_COOKIE_KEYS.FullName) : ''
41
- } catch {
42
- endSession()
43
- }
32
+ return cookieHandler.get(SHARED_COOKIE_KEYS.UserEmail)
44
33
  },
45
34
  getUserId: () => {
46
35
  return cookieHandler.get(SHARED_COOKIE_KEYS.UserId)
@@ -0,0 +1,439 @@
1
+ import { FaAngleDown, FaAngleUp, FaCaretDown, FaKey, FaLock, FaTimes, FaUser } from 'react-icons/fa'
2
+ import { HiMenu } from 'react-icons/hi'
3
+ import { ICompanyConfig_Public, ILayoutTemplateProps } from '../../types/companies'
4
+ import { Link } from 'react-router-dom'
5
+ import { isEncodedURI } from '../../functions/forms'
6
+ import { lazy, Suspense, useCallback, useMemo, useState, useSyncExternalStore } from 'react'
7
+ import { Button, Drawer, Dropdown, Layout } from 'antd'
8
+ import { UserAuth } from '../../api/user'
9
+ import CompanyLogoSection from '../common/company-logo'
10
+ import { MenuItemRenderer, useMenuRenderer } from './template-hooks'
11
+ import { CountryEnum, LOCAL_STORAGE_KEYS_ENUM } from '../../enums'
12
+ import { buildPasswordUiTranslationKey, cookieHandler } from '../../functions'
13
+ import { REACT_QUERY_CLIENT } from '../../constants'
14
+ import { translationStore } from '../common/custom-hooks/use-translation.hook/store'
15
+
16
+ const { Header, Content, Sider } = Layout
17
+ const ChangePasswordModal = lazy(() => import('../modals/change-password.modal'))
18
+
19
+ export type TemplateName = 'template_1' | 'template_2' | 'template_3'
20
+
21
+ export const TemplateLayout = ({
22
+ template,
23
+ children,
24
+ config,
25
+ isPreview = false,
26
+ }: ILayoutTemplateProps & { template: TemplateName }) => {
27
+ const [sidebarOpen, setSidebarOpen] = useState(false)
28
+ const [changePasswordOpen, setChangePasswordOpen] = useState(false)
29
+ const userName = useMemo(() => UserAuth.getUserEmail() || '', [])
30
+ const { selectedLanguage } = useSyncExternalStore(
31
+ translationStore.subscribe.bind(translationStore),
32
+ translationStore.getSnapshot.bind(translationStore),
33
+ )
34
+ const siteConfigs = config?.siteLayout?.siteConfigs
35
+ const navigationWidth = siteConfigs?.custom?.navigationWidth || 200
36
+ const horizontalPadding = siteConfigs?.custom?.horizontalPadding || 20
37
+ const verticalPadding = siteConfigs?.custom?.verticalPadding || 20
38
+ const isCollapsed = navigationWidth < 200
39
+ const logoElement = useMemo(
40
+ () => <CompanyLogoSection logoUrl={config?.siteIdentity?.logoUrl} isPreview={isPreview} />,
41
+ [config?.siteIdentity?.logoUrl, isPreview],
42
+ )
43
+
44
+ const verticalMenuRenderer = useCallback<MenuItemRenderer>(
45
+ ({ form, meta, level, renderChildren, onNavigate }) => {
46
+ const active = isActive(window.location.pathname, meta.href)
47
+ return (
48
+ <div key={form.id}>
49
+ <Link
50
+ to={meta.href}
51
+ target={meta.target}
52
+ onClick={onNavigate}
53
+ style={{
54
+ width: `${navigationWidth - 25}px`,
55
+ borderRadius: `${siteConfigs?.Link.borderRadius}px`,
56
+ backgroundColor: active ? siteConfigs?.Link.colors.activeBackground : siteConfigs?.Link.colors.background,
57
+ color: active ? siteConfigs?.Link.colors.activeText : siteConfigs?.Link.colors.text,
58
+ paddingLeft: `${level * 16 + 20}px`,
59
+ }}
60
+ className={`fc-navigation-menu-item flex items-center gap-x-3 py-2 px-5 leading-4 transition-all duration-200 ${
61
+ active ? 'bg-opacity-25 text-opacity-25 font-semibold' : ''
62
+ }`}
63
+ onMouseEnter={(e) => {
64
+ e.currentTarget.style.backgroundColor = siteConfigs?.Link.colors.hoverBackground || ''
65
+ e.currentTarget.style.color = siteConfigs?.Link.colors.hoverText || ''
66
+ }}
67
+ onMouseLeave={(e) => {
68
+ e.currentTarget.style.backgroundColor = active
69
+ ? siteConfigs?.Link.colors.activeBackground || ''
70
+ : siteConfigs?.Link.colors.background || ''
71
+ e.currentTarget.style.color = active
72
+ ? siteConfigs?.Link.colors.activeText || ''
73
+ : siteConfigs?.Link.colors.text || ''
74
+ }}
75
+ >
76
+ <div className="w-6 h-6 flex items-center justify-center flex-shrink-0">
77
+ {meta.IconComponent ? (
78
+ <meta.IconComponent className="w-4 h-4" />
79
+ ) : (
80
+ <img alt="" src={form.icon ? (form.icon as string) : ''} className="w-4 h-4 object-contain" />
81
+ )}
82
+ </div>
83
+ <span className="flex-grow">{form.displayName || form.name}</span>
84
+ {meta.isParentMenu &&
85
+ (meta.isExpanded ? <FaAngleUp className="w-4 h-4" /> : <FaAngleDown className="w-4 h-4" />)}
86
+ </Link>
87
+ {form.children && !form.isReportGroup && meta.isExpanded && <div className="submenu">{renderChildren()}</div>}
88
+ </div>
89
+ )
90
+ },
91
+ [navigationWidth, siteConfigs],
92
+ )
93
+
94
+ const dropdownMenuRenderer = useCallback<MenuItemRenderer>(
95
+ ({ form, meta, level, renderChildren, onNavigate }) => {
96
+ const active = isActive(window.location.pathname, meta.href)
97
+ return (
98
+ <div key={form.id} className="relative group fc-navigation-menu-item">
99
+ <Link
100
+ to={meta.href}
101
+ target={meta.target}
102
+ onClick={onNavigate}
103
+ style={{
104
+ borderRadius: `${siteConfigs?.Link.borderRadius}px`,
105
+ backgroundColor: active ? siteConfigs?.Link.colors.activeBackground : siteConfigs?.Link.colors.background,
106
+ color: active ? siteConfigs?.Link.colors.activeText : siteConfigs?.Link.colors.text,
107
+ paddingLeft: `${level * 16 + 20}px`,
108
+ }}
109
+ onMouseEnter={(e) => {
110
+ e.currentTarget.style.backgroundColor = siteConfigs?.Link.colors.hoverBackground || ''
111
+ e.currentTarget.style.color = siteConfigs?.Link.colors.hoverText || ''
112
+ }}
113
+ onMouseLeave={(e) => {
114
+ e.currentTarget.style.backgroundColor = active
115
+ ? siteConfigs?.Link.colors.activeBackground || ''
116
+ : siteConfigs?.Link.colors.background || ''
117
+ e.currentTarget.style.color = active
118
+ ? siteConfigs?.Link.colors.activeText || ''
119
+ : siteConfigs?.Link.colors.text || ''
120
+ }}
121
+ className={`flex items-center gap-x-3 py-2 px-5 text-md leading-4 transition-all duration-200 bg-white text-primary hover:bg-opacity-25 hover:text-opacity-25 ${
122
+ active ? 'fc-navigation-menu-item-active font-semibold' : ''
123
+ }`}
124
+ >
125
+ {meta.IconComponent && (
126
+ <div className="w-6 h-6 flex items-center justify-center flex-shrink-0">
127
+ <meta.IconComponent className="w-4 h-4" />
128
+ </div>
129
+ )}
130
+ <span className="flex-grow">{form.displayName || form.name}</span>
131
+ </Link>
132
+ {meta.isParentMenu && (
133
+ <div className="absolute left-0 top-full hidden group-hover:flex flex-col bg-white shadow-md rounded-md z-50 min-w-[180px]">
134
+ {renderChildren()}
135
+ </div>
136
+ )}
137
+ </div>
138
+ )
139
+ },
140
+ [siteConfigs],
141
+ )
142
+
143
+ const compactMenuRenderer = useCallback<MenuItemRenderer>(
144
+ ({ form, meta, level, renderChildren, onNavigate }) => {
145
+ const active = isActive(window.location.pathname, meta.href)
146
+ return (
147
+ <div key={form.id} className="menu-item">
148
+ <Link
149
+ to={meta.href}
150
+ target={meta.target}
151
+ onClick={onNavigate}
152
+ style={{
153
+ width: `${navigationWidth - 25}px`,
154
+ borderRadius: `${siteConfigs?.Link.borderRadius}px`,
155
+ backgroundColor: active ? siteConfigs?.Link.colors.activeBackground : siteConfigs?.Link.colors.background,
156
+ color: active ? siteConfigs?.Link.colors.activeText : siteConfigs?.Link.colors.text,
157
+ paddingLeft: `${level * 16 + 20}px`,
158
+ }}
159
+ className={`fc-navigation-menu-item flex items-center gap-x-3 py-2 px-5 leading-4 transition-all duration-200 ${
160
+ active ? 'bg-opacity-25 text-opacity-25 font-semibold' : ''
161
+ }`}
162
+ onMouseEnter={(e) => {
163
+ e.currentTarget.style.backgroundColor = siteConfigs?.Link.colors.hoverBackground || ''
164
+ e.currentTarget.style.color = siteConfigs?.Link.colors.hoverText || ''
165
+ }}
166
+ onMouseLeave={(e) => {
167
+ e.currentTarget.style.backgroundColor = active
168
+ ? siteConfigs?.Link.colors.activeBackground || ''
169
+ : siteConfigs?.Link.colors.background || ''
170
+ e.currentTarget.style.color = active
171
+ ? siteConfigs?.Link.colors.activeText || ''
172
+ : siteConfigs?.Link.colors.text || ''
173
+ }}
174
+ >
175
+ <div className="w-6 h-6 flex items-center justify-center flex-shrink-0">
176
+ {meta.IconComponent ? (
177
+ <meta.IconComponent className="w-4 h-4" />
178
+ ) : (
179
+ <img alt="" src={form.icon ? (form.icon as string) : ''} className="w-4 h-4 object-contain" />
180
+ )}
181
+ </div>
182
+ <span className="flex-grow">{form.displayName || form.name}</span>
183
+ {meta.isParentMenu &&
184
+ (meta.isExpanded ? <FaAngleUp className="w-4 h-4" /> : <FaAngleDown className="w-4 h-4" />)}
185
+ </Link>
186
+ {form.children && !form.isReportGroup && meta.isExpanded && <div className="submenu">{renderChildren()}</div>}
187
+ </div>
188
+ )
189
+ },
190
+ [navigationWidth, siteConfigs],
191
+ )
192
+
193
+ const rendererMap = useMemo(
194
+ () => ({
195
+ template_1: { renderer: verticalMenuRenderer, options: { enableExpand: true } },
196
+ template_2: { renderer: dropdownMenuRenderer, options: { preventParentNavigation: true } },
197
+ template_3: { renderer: compactMenuRenderer, options: { enableExpand: true } },
198
+ }),
199
+ [verticalMenuRenderer, dropdownMenuRenderer, compactMenuRenderer],
200
+ )
201
+
202
+ const templateRender = rendererMap[template]
203
+ if (!templateRender) return
204
+
205
+ const { renderer, options } = templateRender
206
+ const renderMenuItem = useMenuRenderer(config as ICompanyConfig_Public | undefined, renderer, options)
207
+
208
+ const renderMenuList = useCallback(() => {
209
+ if (!config?.siteMenus?.length)
210
+ return (
211
+ <div className="text-warning text-center bg-warning bg-opacity-25 py-2 rounded-lg italic font-bold text-md">
212
+ No active form is available!
213
+ </div>
214
+ )
215
+ return config.siteMenus.map((form) => renderMenuItem(form))
216
+ }, [config?.siteMenus, renderMenuItem])
217
+
218
+ const drawerContentClass = template === 'template_3' ? 'flex flex-col' : 'flex flex-col overflow-y-auto'
219
+ const mobileDrawer = (
220
+ <Drawer
221
+ open={sidebarOpen}
222
+ onClose={() => setSidebarOpen(false)}
223
+ placement="left"
224
+ width="auto"
225
+ className="relative z-40 lg:hidden"
226
+ closeIcon={
227
+ <button type="button" onClick={() => setSidebarOpen(false)} className="absolute top-5 right-5">
228
+ <FaTimes aria-hidden="true" className="size-6 text-primary" />
229
+ </button>
230
+ }
231
+ >
232
+ <div className={drawerContentClass}>{renderMenuList()}</div>
233
+ </Drawer>
234
+ )
235
+
236
+ const mobileMenuButton = (
237
+ <button type="button" onClick={() => setSidebarOpen(true)} className="-m-2.5 p-2.5 text-gray-700 lg:hidden">
238
+ <span className="sr-only">Open sidebar</span>
239
+ <HiMenu aria-hidden="true" className="size-6" />
240
+ </button>
241
+ )
242
+
243
+ const renderProfileDropdown = (
244
+ trigger: Array<'hover' | 'click'>,
245
+ placement: 'top' | 'bottomRight',
246
+ collapsed?: boolean,
247
+ buttonClass?: string,
248
+ ) => (
249
+ <Dropdown
250
+ menu={{
251
+ items: [
252
+ ...(config?.changePasswordConfig?.isEnabled
253
+ ? [
254
+ {
255
+ key: 'change_password',
256
+ label:
257
+ (selectedLanguage &&
258
+ config?.translations?.[buildPasswordUiTranslationKey('MenuLabel')]?.[
259
+ selectedLanguage as CountryEnum
260
+ ]) ||
261
+ 'Change Password',
262
+ icon: <FaLock />,
263
+ onClick: () => setChangePasswordOpen(true),
264
+ },
265
+ ]
266
+ : []),
267
+ {
268
+ key: 'logout',
269
+ label: 'Log Out',
270
+ icon: <FaKey />,
271
+ onClick: async () => {
272
+ const persistDomain = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.PersistDomain) ?? ''
273
+ cookieHandler.empty()
274
+ REACT_QUERY_CLIENT.clear()
275
+ localStorage.clear()
276
+ localStorage.setItem(LOCAL_STORAGE_KEYS_ENUM.PersistDomain, persistDomain)
277
+ window.location.href = '/login'
278
+ },
279
+ },
280
+ ],
281
+ }}
282
+ trigger={trigger}
283
+ placement={placement}
284
+ >
285
+ <Button type="link" className={buttonClass}>
286
+ {collapsed ? (
287
+ <FaUser />
288
+ ) : (
289
+ <span className="ml-4 text-primary text-md font-semibold">
290
+ <span className="italic text-warning font-normal">{userName}</span>
291
+ </span>
292
+ )}
293
+ <FaCaretDown className="text-primary ml-1" />
294
+ </Button>
295
+ </Dropdown>
296
+ )
297
+
298
+ const layout = (() => {
299
+ if (template === 'template_1') {
300
+ if (isPreview)
301
+ return (
302
+ <div className="border rounded-md w-full">
303
+ <div className="flex items-center justify-between p-2 border-b">
304
+ <span>Logo</span>
305
+ <span>Profile menu</span>
306
+ </div>
307
+ <div className="flex flex-nowrap">
308
+ <div className="p-2 border-r">Menu</div>
309
+ <div className="p-2 bg-background w-full h-[150px]">Content</div>
310
+ </div>
311
+ </div>
312
+ )
313
+
314
+ return (
315
+ <Layout className="min-h-screen overflow-y-auto fc-layout">
316
+ <Header className="sticky top-0 z-40 flex items-center shadow-sm justify-between px-1 overflow-hidden fc-header">
317
+ <div className="flex fc-logo">
318
+ <a href="/">{logoElement}</a>
319
+ {mobileMenuButton}
320
+ </div>
321
+ <div>{renderProfileDropdown(['hover'], 'bottomRight')}</div>
322
+ </Header>
323
+ <Layout>
324
+ <Sider width={navigationWidth} className={`${isPreview ? 'flex' : 'hidden lg:flex'} fc-navigation-bar`}>
325
+ <div className="flex flex-col px-1 gap-2 py-2 fc-navigation-menu">{renderMenuList()}</div>
326
+ </Sider>
327
+ <Content style={{ padding: `${verticalPadding}px ${horizontalPadding}px` }} className="fc-content">
328
+ {children}
329
+ </Content>
330
+ </Layout>
331
+ </Layout>
332
+ )
333
+ }
334
+
335
+ if (template === 'template_2') {
336
+ if (isPreview)
337
+ return (
338
+ <div className="border rounded-md w-full">
339
+ <div className="flex items-center justify-between p-2 border-b">
340
+ <span>Logo</span>
341
+ <span>Menu</span>
342
+ <span>Profile menu</span>
343
+ </div>
344
+ <div className="p-2 bg-background w-full h-[150px]">Content</div>
345
+ </div>
346
+ )
347
+
348
+ return (
349
+ <Layout className="fc-layout min-h-screen">
350
+ <Header className="fc-header sticky top-0 z-40 flex items-center shadow-sm justify-between px-1 overflow-hidden">
351
+ <div className="flex">
352
+ <a href="/">{logoElement}</a>
353
+ {mobileMenuButton}
354
+ </div>
355
+ <div
356
+ className={`fc-navigation-menu justify-center items-center ${isPreview ? 'flex' : 'hidden lg:flex'} py-1`}
357
+ >
358
+ {renderMenuList()}
359
+ </div>
360
+ <div className="fc-profile-menu">{renderProfileDropdown(['hover'], 'bottomRight')}</div>
361
+ </Header>
362
+ <Content style={{ padding: `${verticalPadding}px ${horizontalPadding}px` }} className="fc-content">
363
+ {children}
364
+ </Content>
365
+ </Layout>
366
+ )
367
+ }
368
+
369
+ if (isPreview)
370
+ return (
371
+ <div className="border rounded-md w-full flex flex-row">
372
+ <div className="flex flex-col">
373
+ <div className="border-b p-2">Logo</div>
374
+ <div className="flex flex-col justify-between whitespace-nowrap p-2 h-[150px]">
375
+ <span>Menu</span>
376
+ <span>Profile menu</span>
377
+ </div>
378
+ </div>
379
+ <div className="p-2 bg-background w-full">Content</div>
380
+ </div>
381
+ )
382
+
383
+ return (
384
+ <Layout className="fc-layout min-h-screen">
385
+ <Sider
386
+ width={navigationWidth}
387
+ className={`${
388
+ isPreview ? 'flex' : 'hidden lg:flex'
389
+ } flex-col min-h-screen fixed top-0 left-0 fc-navigation-bar`}
390
+ >
391
+ <div className="flex flex-col p-2 h-full">
392
+ {!isCollapsed && (
393
+ <div className="flex justify-center p-2 fc-logo">
394
+ <a href="/" className="w-full flex justify-center">
395
+ {logoElement}
396
+ </a>
397
+ </div>
398
+ )}
399
+ <div className="flex flex-col py-2 flex-grow fc-navigation-menu">{renderMenuList()}</div>
400
+ </div>
401
+ <div className="fc-profile-menu absolute bottom-2 left-0 w-full flex justify-center">
402
+ {renderProfileDropdown(['click'], 'top', isCollapsed)}
403
+ </div>
404
+ </Sider>
405
+ <Layout style={{ paddingLeft: `${navigationWidth}px` }}>
406
+ <Header className="fc-header lg:hidden sticky top-0 z-40 flex items-center gap-x-4 px-2 shadow-sm">
407
+ <div className="flex p-2 fc-logo">
408
+ <a href="/">{logoElement}</a>
409
+ </div>
410
+ {mobileMenuButton}
411
+ <div className="ml-auto flex items-center fc-profile-menu">
412
+ {renderProfileDropdown(['click'], 'bottomRight', isCollapsed, '-m-1.5 flex items-center p-1.5')}
413
+ </div>
414
+ </Header>
415
+ <Content style={{ padding: `${verticalPadding}px ${horizontalPadding}px` }} className="fc-content">
416
+ {children}
417
+ </Content>
418
+ </Layout>
419
+ </Layout>
420
+ )
421
+ })()
422
+
423
+ return (
424
+ <>
425
+ {changePasswordOpen && (
426
+ <Suspense fallback={null}>
427
+ <ChangePasswordModal closeModal={() => setChangePasswordOpen(false)} />
428
+ </Suspense>
429
+ )}
430
+ {mobileDrawer}
431
+ {layout}
432
+ </>
433
+ )
434
+ }
435
+
436
+ const isActive = (path: string, href: string) => {
437
+ if (isEncodedURI(path)) path = decodeURIComponent(path)
438
+ return path.startsWith(href)
439
+ }