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.
- package/.env.development +2 -1
- package/package.json +1 -1
- package/src/api/client.ts +47 -5
- package/src/api/user.ts +14 -25
- package/src/components/auth-layouts/index.tsx +439 -0
- package/src/components/auth-layouts/template-hooks.tsx +124 -0
- package/src/components/common/custom-hooks/index.ts +1 -0
- package/src/components/common/custom-hooks/use-cache-form-layout-config.hook.ts +2 -2
- package/src/components/common/custom-hooks/use-duplicate-on-blur.hook.ts +185 -185
- package/src/components/common/custom-hooks/use-find-dynamic-form.hook.ts +6 -7
- package/src/components/common/custom-hooks/use-node-condition.hook/use-disabled-elements.hook.ts +30 -3
- package/src/components/common/custom-hooks/use-node-condition.hook/visibility-utils.ts +18 -18
- package/src/components/common/custom-hooks/use-preserved-form-items.hook.ts +13 -5
- package/src/components/common/custom-hooks/use-user-permission.hook.ts +55 -0
- package/src/components/common/duplicate-entry-checker/duplicate-warning.modal.tsx +93 -93
- package/src/components/common/duplicate-entry-checker/index.tsx +54 -54
- package/src/components/common/results/403.tsx +22 -0
- package/src/components/common/{not-found.tsx → results/not-found.tsx} +1 -1
- package/src/components/companies/1-authenticated/index.tsx +13 -11
- package/src/components/companies/index.tsx +0 -1
- package/src/components/form/1-list/table.tsx +1 -1
- package/src/components/form/2-details/index.tsx +102 -22
- package/src/components/form/layout-renderer/1-row/index.tsx +5 -7
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +166 -18
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-action-permissions.hook.ts +82 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-navigate.hook.tsx +2 -2
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-create-data.hook.ts +28 -97
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-save-user-account-action.hook/helper.ts +210 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-save-user-account-action.hook/index.tsx +263 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-send-notification.hook.ts +96 -96
- package/src/components/form/layout-renderer/3-element/10-currency.tsx +28 -6
- package/src/components/form/layout-renderer/3-element/11-breadcrumb/index.tsx +52 -30
- package/src/components/form/layout-renderer/3-element/12-picker-field.tsx +36 -11
- package/src/components/form/layout-renderer/3-element/13-language-selector/index.tsx +33 -10
- package/src/components/form/layout-renderer/3-element/14-auto-complete.tsx +127 -108
- package/src/components/form/layout-renderer/3-element/16-user-role.tsx +30 -8
- package/src/components/form/layout-renderer/3-element/2-field-element.tsx +75 -43
- package/src/components/form/layout-renderer/3-element/3-read-field-data.tsx +17 -2
- package/src/components/form/layout-renderer/3-element/4-rich-text-editor.tsx +16 -2
- package/src/components/form/layout-renderer/3-element/6-signature.tsx +187 -174
- package/src/components/form/layout-renderer/3-element/7-file-upload.tsx +37 -6
- package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +42 -8
- package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +209 -209
- package/src/components/form/layout-renderer/3-element/index.tsx +148 -22
- package/src/components/index.tsx +1 -0
- package/src/components/modals/change-password.modal.tsx +250 -0
- package/src/components/modals/save-user-account.modal.tsx +81 -0
- package/src/{constants.ts → constants/index.ts} +17 -20
- package/src/enums/form.enum.ts +17 -7
- package/src/enums/index.ts +20 -4
- package/src/functions/companies/index.tsx +3 -0
- package/src/functions/companies/translation-key-builder/breadcrumb-translations.ts +5 -0
- package/src/functions/companies/translation-key-builder/password-translations.ts +12 -0
- package/src/functions/companies/translation-key-builder/role-permission-translations.ts +12 -0
- package/src/functions/cookie-handler.ts +3 -2
- package/src/functions/forms/conditional-rule.utils.ts +264 -164
- package/src/functions/forms/conditional-text.ts +48 -0
- package/src/functions/forms/create-form-rules.ts +77 -16
- package/src/functions/forms/index.ts +199 -205
- package/src/types/companies/index.ts +13 -8
- package/src/types/forms/data-list/index.ts +2 -0
- package/src/types/forms/index.ts +35 -34
- package/src/types/forms/layout-elements/button.ts +10 -1
- package/src/types/forms/layout-elements/conditions.ts +20 -0
- package/src/types/forms/layout-elements/index.ts +3 -1
- package/src/types/notifications/index.ts +6 -6
- package/src/components/companies/1-authenticated/change-password.tsx +0 -110
- package/src/types/companies/roles.ts +0 -14
- package/src/types/companies/site-layout/authenticated/index.tsx +0 -736
- 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
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
|
-
|
|
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
|
|
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]
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
const decrypt = (toDecrypt: SHARED_COOKIE_KEYS) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
+
}
|