form-craft-package 1.11.6 → 1.11.9-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.11.6",
3
+ "version": "1.11.9-dev.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
package/src/api/client.ts CHANGED
@@ -208,10 +208,10 @@ export const auth = async (
208
208
  limit: 1,
209
209
  })
210
210
  } catch {}
211
- if (userRolesRes?.data?.totalRecords === 0 && !JSON.parse(loginAuthRes.data.roles).includes('Admin')) {
212
- cookieHandler.empty()
213
- throw { status: 400, response: { status: 400, data: 'User is not found!' } }
214
- }
211
+ if (userRolesRes?.data?.totalRecords === 0 && !JSON.parse(loginAuthRes.data.roles).includes('Admin')) {
212
+ cookieHandler.empty()
213
+ throw { status: 400, response: { status: 400, data: 'User is not found!' } }
214
+ }
215
215
  const fetchedRoles = userRolesRes?.data?.data?.[0]?.Data_roles
216
216
  if (Array.isArray(fetchedRoles))
217
217
  userRoleIds = fetchedRoles.filter((role): role is string => typeof role === 'string' && role.length > 0)
@@ -4,7 +4,7 @@ import { ICompanyConfig_Public, ILayoutTemplateProps } from '../../types/compani
4
4
  import { Link } from 'react-router-dom'
5
5
  import { isEncodedURI } from '../../functions/forms'
6
6
  import { lazy, Suspense, useCallback, useMemo, useState, useSyncExternalStore } from 'react'
7
- import { Button, Drawer, Dropdown, Layout } from 'antd'
7
+ import { Button, Drawer, Dropdown, Grid, Layout } from 'antd'
8
8
  import { UserAuth } from '../../api/user'
9
9
  import CompanyLogoSection from '../common/company-logo'
10
10
  import { MenuItemRenderer, useMenuRenderer } from './template-hooks'
@@ -35,7 +35,12 @@ export const TemplateLayout = ({
35
35
  const navigationWidth = siteConfigs?.custom?.navigationWidth || 200
36
36
  const horizontalPadding = siteConfigs?.custom?.horizontalPadding || 20
37
37
  const verticalPadding = siteConfigs?.custom?.verticalPadding || 20
38
+ const headerHeight = siteConfigs?.Layout?.headerHeight || 60
39
+ const menuPaddingInline = siteConfigs?.Link?.paddingInline ?? 20
40
+ const menuPaddingBlock = siteConfigs?.Link?.paddingBlock ?? 8
38
41
  const isCollapsed = navigationWidth < 200
42
+ const screens = Grid.useBreakpoint()
43
+ const isDesktop = !!screens.lg
39
44
  const logoElement = useMemo(
40
45
  () => <CompanyLogoSection logoUrl={config?.siteIdentity?.logoUrl} isPreview={isPreview} />,
41
46
  [config?.siteIdentity?.logoUrl, isPreview],
@@ -55,9 +60,12 @@ export const TemplateLayout = ({
55
60
  borderRadius: `${siteConfigs?.Link.borderRadius}px`,
56
61
  backgroundColor: active ? siteConfigs?.Link.colors.activeBackground : siteConfigs?.Link.colors.background,
57
62
  color: active ? siteConfigs?.Link.colors.activeText : siteConfigs?.Link.colors.text,
58
- paddingLeft: `${level * 16 + 20}px`,
63
+ paddingTop: `${menuPaddingBlock}px`,
64
+ paddingRight: `${menuPaddingInline}px`,
65
+ paddingBottom: `${menuPaddingBlock}px`,
66
+ paddingLeft: `${level * 16 + menuPaddingInline}px`,
59
67
  }}
60
- className={`fc-navigation-menu-item flex items-center gap-x-3 py-2 px-5 leading-4 transition-all duration-200 ${
68
+ className={`fc-navigation-menu-item flex items-center gap-x-3 leading-4 transition-all duration-200 ${
61
69
  active ? 'bg-opacity-25 text-opacity-25 font-semibold' : ''
62
70
  }`}
63
71
  onMouseEnter={(e) => {
@@ -88,14 +96,14 @@ export const TemplateLayout = ({
88
96
  </div>
89
97
  )
90
98
  },
91
- [navigationWidth, siteConfigs],
99
+ [menuPaddingBlock, menuPaddingInline, navigationWidth, siteConfigs],
92
100
  )
93
101
 
94
102
  const dropdownMenuRenderer = useCallback<MenuItemRenderer>(
95
103
  ({ form, meta, level, renderChildren, onNavigate }) => {
96
104
  const active = isActive(window.location.pathname, meta.href)
97
105
  return (
98
- <div key={form.id} className="relative group fc-navigation-menu-item">
106
+ <div key={form.id} className="group fc-navigation-menu-item">
99
107
  <Link
100
108
  to={meta.href}
101
109
  target={meta.target}
@@ -104,7 +112,10 @@ export const TemplateLayout = ({
104
112
  borderRadius: `${siteConfigs?.Link.borderRadius}px`,
105
113
  backgroundColor: active ? siteConfigs?.Link.colors.activeBackground : siteConfigs?.Link.colors.background,
106
114
  color: active ? siteConfigs?.Link.colors.activeText : siteConfigs?.Link.colors.text,
107
- paddingLeft: `${level * 16 + 20}px`,
115
+ paddingTop: `${menuPaddingBlock}px`,
116
+ paddingRight: `${menuPaddingInline}px`,
117
+ paddingBottom: `${menuPaddingBlock}px`,
118
+ paddingLeft: `${level * 16 + menuPaddingInline}px`,
108
119
  }}
109
120
  onMouseEnter={(e) => {
110
121
  e.currentTarget.style.backgroundColor = siteConfigs?.Link.colors.hoverBackground || ''
@@ -118,7 +129,7 @@ export const TemplateLayout = ({
118
129
  ? siteConfigs?.Link.colors.activeText || ''
119
130
  : siteConfigs?.Link.colors.text || ''
120
131
  }}
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 ${
132
+ className={`flex items-center gap-x-3 text-md leading-4 transition-all duration-200 bg-white text-primary hover:bg-opacity-75 hover:text-opacity-25 ${
122
133
  active ? 'fc-navigation-menu-item-active font-semibold' : ''
123
134
  }`}
124
135
  >
@@ -127,7 +138,7 @@ export const TemplateLayout = ({
127
138
  <meta.IconComponent className="w-4 h-4" />
128
139
  </div>
129
140
  )}
130
- <span className="flex-grow">{form.displayName || form.name}</span>
141
+ <span>{form.displayName || form.name}</span>
131
142
  </Link>
132
143
  {meta.isParentMenu && (
133
144
  <div className="absolute left-0 top-full hidden group-hover:flex flex-col bg-white shadow-md rounded-md z-50 min-w-[180px]">
@@ -137,7 +148,7 @@ export const TemplateLayout = ({
137
148
  </div>
138
149
  )
139
150
  },
140
- [siteConfigs],
151
+ [menuPaddingBlock, menuPaddingInline, siteConfigs],
141
152
  )
142
153
 
143
154
  const compactMenuRenderer = useCallback<MenuItemRenderer>(
@@ -154,9 +165,12 @@ export const TemplateLayout = ({
154
165
  borderRadius: `${siteConfigs?.Link.borderRadius}px`,
155
166
  backgroundColor: active ? siteConfigs?.Link.colors.activeBackground : siteConfigs?.Link.colors.background,
156
167
  color: active ? siteConfigs?.Link.colors.activeText : siteConfigs?.Link.colors.text,
157
- paddingLeft: `${level * 16 + 20}px`,
168
+ paddingTop: `${menuPaddingBlock}px`,
169
+ paddingRight: `${menuPaddingInline}px`,
170
+ paddingBottom: `${menuPaddingBlock}px`,
171
+ paddingLeft: `${level * 16 + menuPaddingInline}px`,
158
172
  }}
159
- className={`fc-navigation-menu-item flex items-center gap-x-3 py-2 px-5 leading-4 transition-all duration-200 ${
173
+ className={`fc-navigation-menu-item flex items-center gap-x-3 leading-4 transition-all duration-200 ${
160
174
  active ? 'bg-opacity-25 text-opacity-25 font-semibold' : ''
161
175
  }`}
162
176
  onMouseEnter={(e) => {
@@ -187,7 +201,7 @@ export const TemplateLayout = ({
187
201
  </div>
188
202
  )
189
203
  },
190
- [navigationWidth, siteConfigs],
204
+ [menuPaddingBlock, menuPaddingInline, navigationWidth, siteConfigs],
191
205
  )
192
206
 
193
207
  const rendererMap = useMemo(
@@ -233,12 +247,13 @@ export const TemplateLayout = ({
233
247
  </Drawer>
234
248
  )
235
249
 
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
- )
250
+ const mobileMenuButton =
251
+ !isPreview && !isDesktop ? (
252
+ <button type="button" onClick={() => setSidebarOpen(true)} className="-m-2.5 block p-2.5 text-gray-700">
253
+ <span className="sr-only">Open sidebar</span>
254
+ <HiMenu aria-hidden="true" className="size-6" />
255
+ </button>
256
+ ) : null
242
257
 
243
258
  const renderProfileDropdown = (
244
259
  trigger: Array<'hover' | 'click'>,
@@ -324,7 +339,14 @@ export const TemplateLayout = ({
324
339
  <Sider width={navigationWidth} className={`${isPreview ? 'flex' : 'hidden lg:flex'} fc-navigation-bar`}>
325
340
  <div className="flex flex-col px-1 gap-2 py-2 fc-navigation-menu">{renderMenuList()}</div>
326
341
  </Sider>
327
- <Content style={{ padding: `${verticalPadding}px ${horizontalPadding}px` }} className="fc-content">
342
+ <Content
343
+ style={{
344
+ padding: `${verticalPadding}px ${horizontalPadding}px`,
345
+ minHeight: `calc(100vh - ${headerHeight}px)`,
346
+ backgroundColor: siteConfigs?.Layout.colors.bodyBg,
347
+ }}
348
+ className="fc-content"
349
+ >
328
350
  {children}
329
351
  </Content>
330
352
  </Layout>
@@ -347,19 +369,29 @@ export const TemplateLayout = ({
347
369
 
348
370
  return (
349
371
  <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">
372
+ <Header
373
+ className="fc-header sticky top-0 z-40 flex items-center shadow-sm justify-between px-1 overflow-visible"
374
+ style={{ height: headerHeight, lineHeight: 'normal' }}
375
+ >
351
376
  <div className="flex">
352
377
  <a href="/">{logoElement}</a>
353
378
  {mobileMenuButton}
354
379
  </div>
355
380
  <div
356
- className={`fc-navigation-menu justify-center items-center ${isPreview ? 'flex' : 'hidden lg:flex'} py-1`}
381
+ className={`fc-navigation-menu justify-center items-center gap-3 px-3 ${isPreview ? 'flex' : 'hidden lg:flex'}`}
357
382
  >
358
383
  {renderMenuList()}
359
384
  </div>
360
385
  <div className="fc-profile-menu">{renderProfileDropdown(['hover'], 'bottomRight')}</div>
361
386
  </Header>
362
- <Content style={{ padding: `${verticalPadding}px ${horizontalPadding}px` }} className="fc-content">
387
+ <Content
388
+ style={{
389
+ padding: `${verticalPadding}px ${horizontalPadding}px`,
390
+ minHeight: `calc(100vh - ${headerHeight}px)`,
391
+ backgroundColor: siteConfigs?.Layout.colors.bodyBg,
392
+ }}
393
+ className="fc-content"
394
+ >
363
395
  {children}
364
396
  </Content>
365
397
  </Layout>
@@ -402,7 +434,7 @@ export const TemplateLayout = ({
402
434
  {renderProfileDropdown(['click'], 'top', isCollapsed)}
403
435
  </div>
404
436
  </Sider>
405
- <Layout style={{ paddingLeft: `${navigationWidth}px` }}>
437
+ <Layout style={{ paddingLeft: `${navigationWidth}px`, minHeight: '100vh', backgroundColor: siteConfigs?.Layout.colors.bodyBg }}>
406
438
  <Header className="fc-header lg:hidden sticky top-0 z-40 flex items-center gap-x-4 px-2 shadow-sm">
407
439
  <div className="flex p-2 fc-logo">
408
440
  <a href="/">{logoElement}</a>
@@ -412,7 +444,14 @@ export const TemplateLayout = ({
412
444
  {renderProfileDropdown(['click'], 'bottomRight', isCollapsed, '-m-1.5 flex items-center p-1.5')}
413
445
  </div>
414
446
  </Header>
415
- <Content style={{ padding: `${verticalPadding}px ${horizontalPadding}px` }} className="fc-content">
447
+ <Content
448
+ style={{
449
+ padding: `${verticalPadding}px ${horizontalPadding}px`,
450
+ minHeight: isDesktop ? '100vh' : `calc(100vh - ${headerHeight}px)`,
451
+ backgroundColor: siteConfigs?.Layout.colors.bodyBg,
452
+ }}
453
+ className="fc-content"
454
+ >
416
455
  {children}
417
456
  </Content>
418
457
  </Layout>
@@ -30,17 +30,17 @@ export function UnauthenticatedLayout() {
30
30
  </div>
31
31
  )
32
32
 
33
+ const loginBackground =
34
+ loginLayout?.backgroundType === 'color'
35
+ ? loginLayout?.background
36
+ : loginLayout?.backgroundType === 'image'
37
+ ? `url(${loginLayout?.backgroundImage}) center/cover no-repeat`
38
+ : loginLayout?.background || '#f0f0f0'
39
+
33
40
  const imageSection = (
34
41
  <div
35
- style={{
36
- background:
37
- loginLayout?.backgroundType === 'color'
38
- ? loginLayout?.background
39
- : loginLayout?.backgroundType === 'image'
40
- ? `url(${loginLayout?.backgroundImage}) center/cover no-repeat`
41
- : '#f0f0f0',
42
- }}
43
- className="w-1/2 h-full flex items-center justify-center align-middle text-center text-gray-400 text-sm bg-gray-100"
42
+ style={{ background: loginBackground }}
43
+ className="w-1/2 h-full flex items-center justify-center align-middle text-center text-gray-400 text-sm"
44
44
  ></div>
45
45
  )
46
46
 
@@ -61,6 +61,8 @@ export function UnauthenticatedLayout() {
61
61
  minWidth: '400px',
62
62
  padding: `${loginLayout?.form?.padding || 16}px`,
63
63
  background: loginLayout?.form?.background,
64
+ borderRadius:
65
+ loginLayout?.formStyle === 'full' ? undefined : `${loginLayout?.form?.borderRadius ?? 12}px`,
64
66
  }}
65
67
  className={`fc-login-panel max-h-screen overflow-auto shadow content-center text-center space-y-4 ${
66
68
  loginLayout?.formStyle === 'full' ? 'h-screen' : ''
@@ -102,6 +104,7 @@ export function UnauthenticatedLayout() {
102
104
  return (
103
105
  <Form.Item
104
106
  style={spacingStyle}
107
+ labelCol={{ style: { paddingBottom: loginLayout?.form?.labelMarginBottom ?? 4 } }}
105
108
  label={field.charAt(0).toUpperCase() + field.slice(1)}
106
109
  name={field}
107
110
  key={field}
@@ -114,6 +117,7 @@ export function UnauthenticatedLayout() {
114
117
  return (
115
118
  <Form.Item
116
119
  style={spacingStyle}
120
+ labelCol={{ style: { paddingBottom: loginLayout?.form?.labelMarginBottom ?? 4 } }}
117
121
  label="Password"
118
122
  name="password"
119
123
  key="password"
@@ -167,10 +171,10 @@ export function UnauthenticatedLayout() {
167
171
  return (
168
172
  <Spin spinning={loading.login}>
169
173
  <div
170
- className={`h-screen w-full flex overflow-hidden ${
174
+ className={`min-h-screen w-screen flex overflow-hidden ${
171
175
  loginLayout?.layout === 'center' ? 'flex-col items-center justify-center' : 'flex-row'
172
176
  }`}
173
- style={{ background: loginLayout?.background || '#f0f0f0' }}
177
+ style={{ background: loginBackground, minHeight: '100vh' }}
174
178
  >
175
179
  {loginLayout?.layout === 'center' && formSection}
176
180
  {loginLayout?.layout === 'split-left' && (
@@ -19,39 +19,41 @@ export function ConfigProviderLayout({ children }: { children: ReactNode }): JSX
19
19
  location.pathname.startsWith('/login')
20
20
 
21
21
  return (
22
- <ConfigContext.Provider value={{ config }}>
23
- <ConfigProvider
24
- theme={{
25
- ...theme,
26
- token: {
27
- ...theme.token,
28
- screenXSMin: 320,
29
- screenXS: 375,
30
- screenXSMax: 425,
31
- screenSMMax: 768,
32
- screenMDMax: 1024,
33
- screenLGMin: 1025,
34
- screenLG: 1100,
35
- screenLGMax: 1200,
36
- screenXLMax: 1440,
37
- },
38
- }}
39
- >
40
- {showSwitchCompany && (
41
- <div className="absolute top-2 right-2 z-20">
42
- <Button_FillerPortal
43
- outline
44
- onClick={() => {
45
- localStorage.removeItem(LOCAL_STORAGE_KEYS_ENUM.Domain)
46
- navigate(0)
47
- }}
48
- >
49
- Switch Project
50
- </Button_FillerPortal>
51
- </div>
52
- )}
53
- {children}
54
- </ConfigProvider>
55
- </ConfigContext.Provider>
22
+ <div className="relative">
23
+ {showSwitchCompany && (
24
+ <div style={{ position: 'absolute', left: 10, top: 10, zIndex: 20, background: 'white', borderRadius: 8 }}>
25
+ <Button_FillerPortal
26
+ outline
27
+ onClick={() => {
28
+ localStorage.removeItem(LOCAL_STORAGE_KEYS_ENUM.Domain)
29
+ navigate(0)
30
+ }}
31
+ >
32
+ Switch Project
33
+ </Button_FillerPortal>
34
+ </div>
35
+ )}
36
+ <ConfigContext.Provider value={{ config }}>
37
+ <ConfigProvider
38
+ theme={{
39
+ ...theme,
40
+ token: {
41
+ ...theme.token,
42
+ screenXSMin: 320,
43
+ screenXS: 375,
44
+ screenXSMax: 425,
45
+ screenSMMax: 768,
46
+ screenMDMax: 1024,
47
+ screenLGMin: 1025,
48
+ screenLG: 1100,
49
+ screenLGMax: 1200,
50
+ screenXLMax: 1440,
51
+ },
52
+ }}
53
+ >
54
+ {children}
55
+ </ConfigProvider>
56
+ </ConfigContext.Provider>
57
+ </div>
56
58
  )
57
59
  }
@@ -9,6 +9,7 @@ import { IDynamicButton_DisplayStateProps } from '../3-element/1-dynamic-button'
9
9
  import { ELEMENTS_DEFAULT_CLASS } from '../../../../constants'
10
10
  import { useHiddenIds } from '../../../common/custom-hooks/use-node-condition.hook/use-node-condition.hook'
11
11
  import { ICustomComponents } from '../3-element'
12
+ import { useConfigContext } from '../../../companies/context'
12
13
 
13
14
  export const LayoutRendererRow = memo(
14
15
  ({
@@ -21,9 +22,26 @@ export const LayoutRendererRow = memo(
21
22
  renderButton,
22
23
  }: ILayoutRendererRow) => {
23
24
  const hiddenIds = useHiddenIds()
25
+ const { config } = useConfigContext()
26
+
27
+ const rowStyle = useMemo(
28
+ () =>
29
+ rowData.styleTemplateId
30
+ ? (config?.styleTemplates?.find((template) => template.id === rowData.styleTemplateId)?.style ?? rowData.style)
31
+ : rowData.style,
32
+ [config?.styleTemplates, rowData.style, rowData.styleTemplateId],
33
+ )
34
+ const headerStyle = useMemo(
35
+ () =>
36
+ rowData.props?.header?.styleTemplateId
37
+ ? (config?.styleTemplates?.find((template) => template.id === rowData.props?.header?.styleTemplateId)?.style ??
38
+ rowData.props?.header?.style)
39
+ : rowData.props?.header?.style,
40
+ [config?.styleTemplates, rowData.props?.header?.style, rowData.props?.header?.styleTemplateId],
41
+ )
24
42
 
25
43
  const gridStyle = useMemo(() => {
26
- const baseStyle = { ...(rowData.style || {}), ...getGridContainerStyle(rowData.display) }
44
+ const baseStyle = { ...(rowStyle || {}), ...getGridContainerStyle(rowData.display) }
27
45
  let template = ''
28
46
 
29
47
  if (typeof baseStyle.gridTemplateColumns === 'string') {
@@ -41,7 +59,7 @@ export const LayoutRendererRow = memo(
41
59
  }
42
60
 
43
61
  return { ...baseStyle, gridTemplateColumns: template }
44
- }, [rowData, hiddenIds])
62
+ }, [rowData, rowStyle, hiddenIds])
45
63
 
46
64
  return (
47
65
  <div
@@ -52,7 +70,7 @@ export const LayoutRendererRow = memo(
52
70
  <LayoutRowConditionalHeaderRenderer
53
71
  formId={formContext.formId}
54
72
  rowId={rowData.id}
55
- header={rowData.props?.header}
73
+ header={rowData.props?.header ? { ...rowData.props.header, style: headerStyle } : undefined}
56
74
  >
57
75
  <LayoutRowRepeatableRenderer
58
76
  formId={formContext.formId}
@@ -64,10 +64,16 @@ export const useSendNotificationAction = ({
64
64
  try {
65
65
  if (!formId || !resolvedFormDataId) return []
66
66
 
67
- const templateReports = formTemplateReports[formId]
67
+ const templateReportsForForm = formTemplateReports?.[formId]
68
+ if (!templateReportsForForm || !Array.isArray(templateReportsForForm.templates)) {
69
+ console.warn(
70
+ `Notification attachment generation skipped: templateReportConfig is missing for form ${formId}.`,
71
+ )
72
+ return []
73
+ }
68
74
 
69
75
  const joinedFormData = await client
70
- .post(`/api/report/${formId}/${resolvedFormDataId}`, { joins: templateReports.joins })
76
+ .post(`/api/report/${formId}/${resolvedFormDataId}`, { joins: templateReportsForForm.joins || [] })
71
77
  .then((res) => (res.status === 200 ? res.data : {}))
72
78
  .catch(() => ({}))
73
79
 
@@ -97,10 +103,15 @@ export const useSendNotificationAction = ({
97
103
  for (const attachment of attachments) {
98
104
  const resolvedBlobName = resolveAttachmentBlobName(attachment)
99
105
  if (!resolvedBlobName) continue
100
- const selectedTemplateInfo = templateReports.templates.find(
106
+ const selectedTemplateInfo = templateReportsForForm.templates.find(
101
107
  (tR: IFormTemplateReport) => tR.fileBlobName === resolvedBlobName,
102
108
  )
103
- if (!selectedTemplateInfo) continue
109
+ if (!selectedTemplateInfo) {
110
+ console.warn(
111
+ `Notification attachment generation skipped: template report "${resolvedBlobName}" was not found in form ${formId} templateReportConfig.`,
112
+ )
113
+ continue
114
+ }
104
115
 
105
116
  const generatedAttachment = await replaceTemplateReportAndUpload({
106
117
  templateInfo: selectedTemplateInfo,
@@ -1,132 +1,132 @@
1
- import { IBreadcrumb, useBreadcrumb } from '../../../../common/custom-hooks/use-breadcrumb.hook'
2
- import { Button_FillerPortal } from '../../../../common/button'
3
- import { useNavigate } from 'react-router-dom'
4
- import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'
5
- import React, { memo, useMemo, useSyncExternalStore } from 'react'
6
- import { ELEMENTS_DEFAULT_CLASS } from '../../../../../constants'
7
- import { IElementBaseProps } from '../'
8
- import useGetCurrentBreakpoint from '../../../../common/custom-hooks/use-window-width.hook'
9
- import { Select } from 'antd'
10
- import { CountryEnum, DeviceBreakpointEnum, PageViewTypEnum } from '../../../../../enums'
11
- import { useConfigContext } from '../../../../companies/context'
12
- import { buildBreadcrumbTranslationKey } from '../../../../../functions'
13
- import { translationStore } from '../../../../common/custom-hooks/use-translation.hook/store'
14
-
15
- import './index.scss'
16
-
17
- function LayoutRenderer_Breadcrumb(_: { formId?: number } & IElementBaseProps) {
18
- const currentBreakpoint = useGetCurrentBreakpoint()
19
- const navigate = useNavigate()
20
- const { breadcrumbs, sliceCrumbAt } = useBreadcrumb()
21
- const { config } = useConfigContext()
22
- const { selectedLanguage } = useSyncExternalStore(
23
- translationStore.subscribe.bind(translationStore),
24
- translationStore.getSnapshot.bind(translationStore),
25
- )
26
-
27
- const translations = config?.translations
28
- const fallbackTexts = { Detail: 'detail', List: 'list', New: 'New' }
29
- const getTranslation = useMemo(
30
- () => (key: string) => (selectedLanguage ? translations?.[key]?.[selectedLanguage as CountryEnum] ?? '' : ''),
31
- [selectedLanguage, translations],
32
- )
33
- const getFormName = useMemo(
34
- () =>
35
- (bc: IBreadcrumb, field: 'Detail' | 'List' | 'New') =>
36
- bc.formId
37
- ? getTranslation(buildBreadcrumbTranslationKey(bc.formId, field)) || bc.formName?.split(' ')?.[0] || bc.label
38
- : bc.label,
39
- [getTranslation],
40
- )
41
- const getBreadcrumbText = useMemo(
42
- () =>
43
- (field: 'Detail' | 'List' | 'New') => {
44
- const defaultText = getTranslation(buildBreadcrumbTranslationKey('Default', field))
45
- return defaultText || fallbackTexts[field]
46
- },
47
- [getTranslation],
48
- )
49
-
50
- const showAsSelect = useMemo(
51
- () =>
52
- ([DeviceBreakpointEnum.Mobile, DeviceBreakpointEnum.TabletPortrait].includes(currentBreakpoint) &&
53
- breadcrumbs.length > 1) ||
54
- breadcrumbs.length > 3,
55
- [currentBreakpoint, breadcrumbs],
56
- )
57
-
58
- if (showAsSelect)
59
- return (
60
- <Select
61
- suffixIcon={null}
62
- value={breadcrumbs[breadcrumbs.length - 1].href}
63
- optionFilterProp="label"
64
- allowClear={false}
65
- className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} responsive max-w-[250px]`}
66
- onChange={(bc) => {
67
- const decodedUri = decodeURIComponent(bc)
68
- const bcIdx = breadcrumbs.findIndex((bc) => bc.href === decodedUri)
69
-
70
- sliceCrumbAt(bcIdx)
71
- navigate(decodedUri)
72
- }}
73
- options={breadcrumbs.map((bc) => ({
74
- value: bc.href,
75
- label: (
76
- <div className="flex gap-2 items-center">
77
- <FaChevronLeft className="text-primary" size={10} />
78
- {renderText(bc, getFormName, getBreadcrumbText)}
79
- </div>
80
- ),
81
- }))}
82
- />
83
- )
84
-
85
- return (
86
- <div className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} flex items-center text-12`}>
87
- {breadcrumbs.map((bc, bcIdx) => (
88
- <React.Fragment key={bcIdx}>
89
- <Button_FillerPortal
90
- link
91
- disabled={breadcrumbs.length - 1 === bcIdx}
92
- onClick={() => {
93
- sliceCrumbAt(bcIdx)
94
- navigate(bc.href)
95
- }}
96
- >
97
- {renderText(bc, getFormName, getBreadcrumbText)}
98
- </Button_FillerPortal>
99
- {breadcrumbs.length - 1 !== bcIdx && <FaChevronRight className="text-primary" />}
100
- </React.Fragment>
101
- ))}
102
- </div>
103
- )
104
- }
105
-
106
- export default memo(LayoutRenderer_Breadcrumb)
107
-
108
- const renderText = (
109
- bc: IBreadcrumb,
110
- getFormName: (bc: IBreadcrumb, field: 'Detail' | 'List' | 'New') => string,
111
- getBreadcrumbText: (field: 'Detail' | 'List' | 'New') => string,
112
- ) => {
113
- const label =
114
- bc.type === PageViewTypEnum.Details
115
- ? getFormName(bc, 'Detail')
116
- : bc.type === PageViewTypEnum.List
117
- ? getFormName(bc, 'List')
118
- : bc.type === PageViewTypEnum.New
119
- ? getFormName(bc, 'New')
120
- : bc.label
121
- const detailText = getBreadcrumbText('Detail')
122
- const listText = getBreadcrumbText('List')
123
- const newText = getBreadcrumbText('New')
124
-
125
- return (
126
- <span className="font-normal italic">
127
- {bc.type === PageViewTypEnum.New ? `${newText} ` : ''}
128
- {label}
129
- {bc.type === PageViewTypEnum.Details ? ` ${detailText}` : bc.type === PageViewTypEnum.List ? ` ${listText}` : ''}
130
- </span>
131
- )
1
+ import { IBreadcrumb, useBreadcrumb } from '../../../../common/custom-hooks/use-breadcrumb.hook'
2
+ import { Button_FillerPortal } from '../../../../common/button'
3
+ import { useNavigate } from 'react-router-dom'
4
+ import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'
5
+ import React, { memo, useMemo, useSyncExternalStore } from 'react'
6
+ import { ELEMENTS_DEFAULT_CLASS } from '../../../../../constants'
7
+ import { IElementBaseProps } from '../'
8
+ import useGetCurrentBreakpoint from '../../../../common/custom-hooks/use-window-width.hook'
9
+ import { Select } from 'antd'
10
+ import { CountryEnum, DeviceBreakpointEnum, PageViewTypEnum } from '../../../../../enums'
11
+ import { useConfigContext } from '../../../../companies/context'
12
+ import { buildBreadcrumbTranslationKey } from '../../../../../functions'
13
+ import { translationStore } from '../../../../common/custom-hooks/use-translation.hook/store'
14
+
15
+ import './index.scss'
16
+
17
+ function LayoutRenderer_Breadcrumb(_: { formId?: number } & IElementBaseProps) {
18
+ const currentBreakpoint = useGetCurrentBreakpoint()
19
+ const navigate = useNavigate()
20
+ const { breadcrumbs, sliceCrumbAt } = useBreadcrumb()
21
+ const { config } = useConfigContext()
22
+ const { selectedLanguage } = useSyncExternalStore(
23
+ translationStore.subscribe.bind(translationStore),
24
+ translationStore.getSnapshot.bind(translationStore),
25
+ )
26
+
27
+ const translations = config?.translations
28
+ const fallbackTexts = { Detail: 'detail', List: 'list', New: 'New' }
29
+ const getTranslation = useMemo(
30
+ () => (key: string) => (selectedLanguage ? translations?.[key]?.[selectedLanguage as CountryEnum] ?? '' : ''),
31
+ [selectedLanguage, translations],
32
+ )
33
+ const getFormName = useMemo(
34
+ () =>
35
+ (bc: IBreadcrumb, field: 'Detail' | 'List' | 'New') =>
36
+ bc.formId
37
+ ? getTranslation(buildBreadcrumbTranslationKey(bc.formId, field)) || bc.formName?.split(' ')?.[0] || bc.label
38
+ : bc.label,
39
+ [getTranslation],
40
+ )
41
+ const getBreadcrumbText = useMemo(
42
+ () =>
43
+ (field: 'Detail' | 'List' | 'New') => {
44
+ const defaultText = getTranslation(buildBreadcrumbTranslationKey('Default', field))
45
+ return defaultText || fallbackTexts[field]
46
+ },
47
+ [getTranslation],
48
+ )
49
+
50
+ const showAsSelect = useMemo(
51
+ () =>
52
+ ([DeviceBreakpointEnum.Mobile, DeviceBreakpointEnum.TabletPortrait].includes(currentBreakpoint) &&
53
+ breadcrumbs.length > 1) ||
54
+ breadcrumbs.length > 3,
55
+ [currentBreakpoint, breadcrumbs],
56
+ )
57
+
58
+ if (showAsSelect)
59
+ return (
60
+ <Select
61
+ suffixIcon={null}
62
+ value={breadcrumbs[breadcrumbs.length - 1].href}
63
+ optionFilterProp="label"
64
+ allowClear={false}
65
+ className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} responsive max-w-[250px]`}
66
+ onChange={(bc) => {
67
+ const decodedUri = decodeURIComponent(bc)
68
+ const bcIdx = breadcrumbs.findIndex((bc) => bc.href === decodedUri)
69
+
70
+ sliceCrumbAt(bcIdx)
71
+ navigate(decodedUri)
72
+ }}
73
+ options={breadcrumbs.map((bc) => ({
74
+ value: bc.href,
75
+ label: (
76
+ <div className="flex gap-2 items-center">
77
+ <FaChevronLeft className="text-primary" size={10} />
78
+ {renderText(bc, getFormName, getBreadcrumbText)}
79
+ </div>
80
+ ),
81
+ }))}
82
+ />
83
+ )
84
+
85
+ return (
86
+ <div className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} flex items-center text-12`}>
87
+ {breadcrumbs.map((bc, bcIdx) => (
88
+ <React.Fragment key={bcIdx}>
89
+ <Button_FillerPortal
90
+ link
91
+ disabled={breadcrumbs.length - 1 === bcIdx}
92
+ onClick={() => {
93
+ sliceCrumbAt(bcIdx)
94
+ navigate(bc.href)
95
+ }}
96
+ >
97
+ {renderText(bc, getFormName, getBreadcrumbText)}
98
+ </Button_FillerPortal>
99
+ {breadcrumbs.length - 1 !== bcIdx && <FaChevronRight className="text-primary" />}
100
+ </React.Fragment>
101
+ ))}
102
+ </div>
103
+ )
104
+ }
105
+
106
+ export default memo(LayoutRenderer_Breadcrumb)
107
+
108
+ const renderText = (
109
+ bc: IBreadcrumb,
110
+ getFormName: (bc: IBreadcrumb, field: 'Detail' | 'List' | 'New') => string,
111
+ getBreadcrumbText: (field: 'Detail' | 'List' | 'New') => string,
112
+ ) => {
113
+ const label =
114
+ bc.type === PageViewTypEnum.Details
115
+ ? getFormName(bc, 'Detail')
116
+ : bc.type === PageViewTypEnum.List
117
+ ? getFormName(bc, 'List')
118
+ : bc.type === PageViewTypEnum.New
119
+ ? getFormName(bc, 'New')
120
+ : bc.label
121
+ const detailText = getBreadcrumbText('Detail')
122
+ const listText = getBreadcrumbText('List')
123
+ const newText = getBreadcrumbText('New')
124
+
125
+ return (
126
+ <span className="font-normal italic">
127
+ {bc.type === PageViewTypEnum.New ? `${newText} ` : ''}
128
+ {label}
129
+ {bc.type === PageViewTypEnum.Details ? ` ${detailText}` : bc.type === PageViewTypEnum.List ? ` ${listText}` : ''}
130
+ </span>
131
+ )
132
132
  }
@@ -4,7 +4,7 @@ import { saveFile } from '../../../../../functions/forms/form'
4
4
  import { TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../../enums'
5
5
  import { RcFile, UploadFile, UploadProps } from 'antd/es/upload'
6
6
  import { Form, Modal, Spin, Upload } from 'antd'
7
- import { useCallback, useEffect, useMemo, useState } from 'react'
7
+ import { useCallback, useEffect, useMemo, useState } from 'react'
8
8
  import { FaUpload } from 'react-icons/fa6'
9
9
  import { Button_FillerPortal } from '../../../../common/button'
10
10
  import { useNotification, useTranslation } from '../../../../common/custom-hooks'
@@ -77,19 +77,19 @@ export default function GalleryUploadModal({
77
77
  [elementKey, label, placeholder, t],
78
78
  )
79
79
 
80
- const uploadedBlobNames = useMemo(
80
+ const uploadedBlobNames = useMemo(
81
81
  () =>
82
82
  uploadedFiles
83
83
  .map((file) =>
84
84
  typeof file.response === 'object' && file.response ? (file.response as { blobName?: string }).blobName : '',
85
85
  )
86
86
  .filter((blobName): blobName is string => !!blobName),
87
- [uploadedFiles],
88
- )
89
-
90
- useEffect(() => {
91
- if (!isOpen) setUploadedFiles([])
92
- }, [isOpen])
87
+ [uploadedFiles],
88
+ )
89
+
90
+ useEffect(() => {
91
+ if (!isOpen) setUploadedFiles([])
92
+ }, [isOpen])
93
93
 
94
94
  const uploadProps: UploadProps = {
95
95
  multiple: true,
@@ -112,14 +112,14 @@ export default function GalleryUploadModal({
112
112
  },
113
113
  }
114
114
 
115
- const discardUploadedFiles = useCallback(async () => {
116
- setIsDiscarding(true)
117
- try {
118
- await Promise.all(uploadedBlobNames.map((blobName) => client.delete(`/api/attachment/${blobName}`)))
119
- setUploadedFiles([])
120
- onDiscardSuccess()
121
- } finally {
122
- setIsDiscarding(false)
115
+ const discardUploadedFiles = useCallback(async () => {
116
+ setIsDiscarding(true)
117
+ try {
118
+ await Promise.all(uploadedBlobNames.map((blobName) => client.delete(`/api/attachment/${blobName}`)))
119
+ setUploadedFiles([])
120
+ onDiscardSuccess()
121
+ } finally {
122
+ setIsDiscarding(false)
123
123
  }
124
124
  }, [onDiscardSuccess, uploadedBlobNames])
125
125
 
@@ -140,19 +140,19 @@ export default function GalleryUploadModal({
140
140
  ...(Array.isArray(existingBlobNames) ? existingBlobNames : []),
141
141
  ...uploadedBlobNames,
142
142
  ]
143
- const res = await client.put(`/api/formdata/${formId}/${formDataId}`, {
144
- name: formDataRes.data.name,
145
- version: formDataRes.data.version,
146
- private: formDataRes.data.private,
147
- data: JSON.stringify(parsedData),
148
- })
149
- if (res.status === 200) {
150
- setUploadedFiles([])
151
- onSaveSuccess(uploadedBlobNames)
152
- }
153
- } finally {
154
- setIsSaving(false)
155
- }
143
+ const res = await client.put(`/api/formdata/${formId}/${formDataId}`, {
144
+ name: formDataRes.data.name,
145
+ version: formDataRes.data.version,
146
+ private: formDataRes.data.private,
147
+ data: JSON.stringify(parsedData),
148
+ })
149
+ if (res.status === 200) {
150
+ setUploadedFiles([])
151
+ onSaveSuccess(uploadedBlobNames)
152
+ }
153
+ } finally {
154
+ setIsSaving(false)
155
+ }
156
156
  }, [formDataId, formId, formItem.path, onSaveSuccess, uploadedBlobNames])
157
157
 
158
158
  return (
@@ -282,9 +282,9 @@ const fieldRenderers: Partial<Record<ElementTypeEnum, FieldRenderer>> = {
282
282
  [ElementTypeEnum.Radio]: ({ options = [], disabled, isMultiValue, isCustom }) =>
283
283
  isMultiValue ? (
284
284
  <Checkbox.Group disabled={disabled} className={ELEMENTS_DEFAULT_CLASS.CheckboxGroup}>
285
- <div className="flex items-center gap-1">
285
+ <div className="flex items-center gap-1 flex-wrap">
286
286
  {options.map((o, i) => (
287
- <Checkbox key={i} value={o.value}>
287
+ <Checkbox key={i} value={o.value} className='whitespace-nowrap'>
288
288
  {o.label}
289
289
  </Checkbox>
290
290
  ))}
@@ -139,6 +139,7 @@ function LayoutRendererElement({
139
139
 
140
140
  const { t, selectedLanguage } = useTranslation(formContext.formId)
141
141
  const { config } = useConfigContext()
142
+ const styleTemplates = config?.styleTemplates
142
143
  const { maxValue, minValue } = findMaxAndMinValues(validations, {
143
144
  fieldValues: (formContext.formRef?.getFieldsValue(true) ?? {}) as Record<string, any>,
144
145
  formDataId: formContext.formDataId,
@@ -146,6 +147,8 @@ function LayoutRendererElement({
146
147
  })
147
148
 
148
149
  const isElementDisabled = useIsNodeDisabled(key)
150
+ const resolveStyleTemplate = (styleTemplateId?: string) =>
151
+ styleTemplateId ? styleTemplates?.find((template) => template.id === styleTemplateId)?.style : undefined
149
152
 
150
153
  const validationsAfterIsHidden = useMemo(
151
154
  () => (isHidden && validations ? validations.filter((v) => v.rule !== FieldValidationEnum.Required) : validations),
@@ -320,7 +323,8 @@ function LayoutRendererElement({
320
323
  },
321
324
 
322
325
  [ElementTypeEnum.ReadFieldData]: () => {
323
- let style = (elementData as IReadFieldDataElement).style?.[currentBreakpoint]
326
+ let style = resolveStyleTemplate((elementData as IReadFieldDataElement).styleTemplateId)
327
+ if (!style) style = (elementData as IReadFieldDataElement).style?.[currentBreakpoint]
324
328
  if (!style) style = (elementData as IReadFieldDataElement).style?.[DeviceBreakpointEnum.Default]
325
329
 
326
330
  return (
@@ -352,7 +356,8 @@ function LayoutRendererElement({
352
356
  ),
353
357
 
354
358
  [ElementTypeEnum.Text]: () => {
355
- let style = (elementData as ITextElement).style?.[currentBreakpoint]
359
+ let style = resolveStyleTemplate((elementData as ITextElement).styleTemplateId)
360
+ if (!style) style = (elementData as ITextElement).style?.[currentBreakpoint]
356
361
  if (!style) style = (elementData as ITextElement).style?.[DeviceBreakpointEnum.Default]
357
362
 
358
363
  if (props.type === TextElementTypeEnum.Plain)
@@ -48,6 +48,9 @@ export const DEFAULT_SITE_CONFIG = {
48
48
  },
49
49
  Link: {
50
50
  borderRadius: 8,
51
+ paddingInline: 20,
52
+ paddingBlock: 8,
53
+ activeBorderRadius: 8,
51
54
  colors: {
52
55
  background: '#ffffff',
53
56
  text: '#000000',
@@ -55,6 +58,7 @@ export const DEFAULT_SITE_CONFIG = {
55
58
  hoverText: '#245780',
56
59
  activeBackground: '#245780',
57
60
  activeText: '#f5f5f5',
61
+ activeBorderColor: '#1b4566',
58
62
  },
59
63
  },
60
64
  Layout: {
@@ -95,12 +99,21 @@ export const DEFAULT_SITE_CONFIG = {
95
99
  bodySortBg: '#fafafa',
96
100
  colorBgContainer: '#ffffff',
97
101
  rowHoverBg: '#fafafa',
102
+ rowSelectedBg: '#e6f4ff',
103
+ rowSelectedHoverBg: '#d9efff',
104
+ rowExpandedBg: '#fafcff',
98
105
  borderColor: '#f0f0f0',
99
106
  headerSplitColor: '#f0f0f0',
100
107
  headerBg: '#C6D4E2AA',
101
108
  headerColor: '#245780',
102
109
  colorText: '#000000',
103
110
  headerSortActiveBg: '#f0f0f0',
111
+ headerSortHoverBg: '#f5f5f5',
112
+ footerBg: '#fafafa',
113
+ footerColor: '#000000',
114
+ fixedHeaderSortActiveBg: '#e6edf5',
115
+ filterDropdownMenuBg: '#ffffff',
116
+ stickyScrollBarBg: 'rgba(0, 0, 0, 0.25)',
104
117
  },
105
118
  headerBorderRadius: 8,
106
119
  cellPaddingInline: 16,
@@ -108,8 +121,9 @@ export const DEFAULT_SITE_CONFIG = {
108
121
  lineHeight: 1.57,
109
122
  lineWidth: 1,
110
123
  cellFontSize: 14,
111
- padding: 16,
112
- margin: 16,
124
+ stickyScrollBarBorderRadius: 100,
125
+ selectionColumnWidth: 32,
126
+ expandIconMarginTop: 16,
113
127
  },
114
128
  Form: {
115
129
  colors: {
@@ -117,6 +131,9 @@ export const DEFAULT_SITE_CONFIG = {
117
131
  labelRequiredMarkColor: '#ff4d4f',
118
132
  },
119
133
  labelFontSize: 14,
134
+ itemMarginBottom: 24,
135
+ verticalLabelPadding: '0 0 8px',
136
+ labelHeight: 32,
120
137
  },
121
138
  },
122
139
  }
@@ -145,6 +162,8 @@ export const DEFAULT_COMPANY_CONFIG = {
145
162
  width: 400,
146
163
  weight: 700,
147
164
  padding: 20,
165
+ borderRadius: 12,
166
+ labelMarginBottom: 4,
148
167
  spacing: 20,
149
168
  fields: ['email', 'password', 'forgot-password'],
150
169
  },
@@ -47,6 +47,14 @@ export enum TextElementTypeEnum {
47
47
  Plain = 'Plain',
48
48
  Rich = 'Rich',
49
49
  }
50
+ export enum StyleTemplateTypeEnum {
51
+ ContainerStyle = 'ContainerStyle',
52
+ ContainerHeaderStyle = 'ContainerHeaderStyle',
53
+ TextStyle = 'TextStyle',
54
+ ReportTableStyle = 'ReportTableStyle',
55
+ ListStyle = 'ListStyle',
56
+ ImageStyle = 'ImageStyle',
57
+ }
50
58
  export enum DndLayoutNodeEnum { // when saved, everything in the layout will have either of these node types
51
59
  Element = 'Element', // it's the actual display of the form (can be a field, a divider etc.)
52
60
  Row = 'Row',
@@ -244,16 +244,25 @@ export const replaceAndGetFileBlob = async (
244
244
  companyKey?: string
245
245
  formId?: number
246
246
  },
247
- ): Promise<string> => {
247
+ ): Promise<Blob | ''> => {
248
248
  if (!contextData?.companyKey) return ''
249
249
 
250
250
  try {
251
251
  const replacements = await Promise.all(
252
252
  tInfo.replacements.map(async (rep) => {
253
+ if (!rep.renderConfig) rep.renderConfig = { type: DataRenderTypeEnum.Default }
254
+
255
+ if (!rep.field) {
256
+ return {
257
+ placeholder: rep.placeholder,
258
+ value: '',
259
+ type: rep.type,
260
+ }
261
+ }
262
+
253
263
  const fieldPath: string[] = rep.field.split('.')
254
264
  let repValue = fieldPath.reduce((curr, n) => curr?.[n] ?? {}, formData)
255
265
 
256
- if (!rep.renderConfig) rep.renderConfig = { type: DataRenderTypeEnum.Default }
257
266
 
258
267
  if (typeof repValue === 'object') {
259
268
  if (MongoDbExtendedJsonObjectKeys.Date in repValue) repValue = repValue[MongoDbExtendedJsonObjectKeys.Date]
@@ -371,8 +380,13 @@ export const replaceTemplateReportAndUpload = async ({
371
380
  const replacedFileBlob = await replaceAndGetFileBlob(templateInfo, formData, { companyKey, formId })
372
381
  if (!replacedFileBlob) return null
373
382
 
383
+ const resolvedFileName = fileNameHint ?? templateInfo.templateName
384
+ const normalizedFileName = resolvedFileName.toLowerCase().endsWith('.pdf')
385
+ ? resolvedFileName
386
+ : `${resolvedFileName.replace(/\.[^./\]]+$/, '')}.pdf`
387
+
374
388
  const uploadPayload = new FormData()
375
- uploadPayload.append('file', replacedFileBlob)
389
+ uploadPayload.append('file', replacedFileBlob, normalizedFileName)
376
390
  const uploadUrl = isPublic ? `/api/attachment/create/${companyKey}` : '/api/attachment'
377
391
 
378
392
  const blobName = await client
@@ -382,11 +396,6 @@ export const replaceTemplateReportAndUpload = async ({
382
396
 
383
397
  if (!blobName) return null
384
398
 
385
- const resolvedFileName = fileNameHint ?? templateInfo.templateName
386
- const normalizedFileName = resolvedFileName.toLowerCase().endsWith('.pdf')
387
- ? resolvedFileName
388
- : `${resolvedFileName.replace(/\.[^./\]]+$/, '')}.pdf`
389
-
390
399
  return { blobName, fileName: normalizedFileName }
391
400
  } catch (err) {
392
401
  console.error('replaceTemplateReportAndUpload failed:', err)
@@ -1,6 +1,6 @@
1
1
  import { IconType } from 'react-icons'
2
- import { SystemEnvironmentEnum, SystemRolePermissionEnum } from '../../enums'
3
- import { IFormTranslations } from '../forms'
2
+ import { StyleTemplateTypeEnum, SystemEnvironmentEnum, SystemRolePermissionEnum } from '../../enums'
3
+ import { ICssStyle, IFormTranslations } from '../forms'
4
4
 
5
5
  export interface ICompanyViewModel {
6
6
  id: number
@@ -24,8 +24,15 @@ export interface ICompanyConfig_Public {
24
24
  systemRoles: ISystemRole[]
25
25
  changePasswordConfig?: ICompanyConfig_ChangePasswordConfig
26
26
  translations: ICompanyTranslations
27
+ styleTemplates?: IStyleTemplate[]
27
28
  }
28
29
  export type ICompanyTranslations = IFormTranslations
30
+ export interface IStyleTemplate {
31
+ id: string
32
+ name: string
33
+ type: StyleTemplateTypeEnum
34
+ style: ICssStyle
35
+ }
29
36
  export interface ISystemRole {
30
37
  id: string
31
38
  name: string
@@ -43,6 +50,8 @@ export interface ILoginLayout {
43
50
  export interface ILoginFormLayout {
44
51
  width: number
45
52
  padding: number
53
+ borderRadius?: number
54
+ labelMarginBottom?: number
46
55
  title: string
47
56
  background: string
48
57
  size: number
@@ -136,6 +145,9 @@ export interface ICompanyConfig_LayoutConfig {
136
145
  export interface ICompanyConfig_LinkConfig {
137
146
  colors: ICompanyConfig_SiteColors
138
147
  borderRadius: number
148
+ paddingInline?: number
149
+ paddingBlock?: number
150
+ activeBorderRadius?: number
139
151
  }
140
152
  export interface ICompanyConfig_CustomButtonColors {
141
153
  colorTextDisabled?: string
@@ -187,12 +199,16 @@ export interface ICompanyConfig_TableConfig {
187
199
  lineHeight?: number
188
200
  lineWidth?: number
189
201
  cellFontSize?: number
190
- padding?: number
191
- margin?: number
202
+ stickyScrollBarBorderRadius?: number
203
+ selectionColumnWidth?: number
204
+ expandIconMarginTop?: number
192
205
  }
193
206
  export interface ICompanyConfig_FormConfig {
194
207
  colors: ICompanyConfig_SiteColors
195
208
  labelFontSize?: number
209
+ itemMarginBottom?: number
210
+ verticalLabelPadding?: string
211
+ labelHeight?: number
196
212
  }
197
213
  export interface ICompanyConfig_SiteConfigs {
198
214
  fontFamily: string
@@ -101,6 +101,7 @@ export interface IFormDndLayoutRowHeader {
101
101
  isCollapsible?: boolean
102
102
  defaultCollapsed?: boolean
103
103
  style?: { [key: string]: string | number }
104
+ styleTemplateId?: string
104
105
  }
105
106
  export interface IRepeatingSectionConfig {
106
107
  name: string
@@ -226,6 +226,7 @@ export interface IPlaceholderElement extends BaseFormLayoutElement {
226
226
  export interface IReportTableElement extends BaseFormLayoutElement {
227
227
  elementType: ElementTypeEnum.ReportTable
228
228
  style?: ICssStyle | ICssStylePerBreakpoint
229
+ styleTemplateId?: string
229
230
  props: IReportTableElementProps
230
231
  }
231
232
 
@@ -258,6 +259,7 @@ export interface IDividerElementProps {
258
259
  export interface IReadFieldDataElement extends BaseFormLayoutElement {
259
260
  elementType: ElementTypeEnum.ReadFieldData
260
261
  style?: ICssStyle | ICssStylePerBreakpoint
262
+ styleTemplateId?: string
261
263
  props: IReadFieldDataElementProps
262
264
  }
263
265
 
@@ -273,6 +275,7 @@ export interface IButtonElement extends BaseFormLayoutElement {
273
275
  export interface ITextElement extends BaseFormLayoutElement {
274
276
  elementType: ElementTypeEnum.Text
275
277
  style?: ICssStyle | ICssStylePerBreakpoint
278
+ styleTemplateId?: string
276
279
  props: ITextElementProps
277
280
  }
278
281
  export interface ITextElementProps {
@@ -328,6 +331,7 @@ export interface IColorPickerElement extends BaseFormLayoutElement {
328
331
  export interface IListElement extends BaseFormLayoutElement {
329
332
  elementType: ElementTypeEnum.OrderedList | ElementTypeEnum.UnorderedList
330
333
  style?: ICssStyle
334
+ styleTemplateId?: string
331
335
  props: {
332
336
  pageBreak?: ReportPageBreakEnum
333
337
  }
@@ -338,6 +342,7 @@ export interface IListElement extends BaseFormLayoutElement {
338
342
  export interface IImageElement extends BaseFormLayoutElement {
339
343
  elementType: ElementTypeEnum.Image
340
344
  style?: ICssStyle
345
+ styleTemplateId?: string
341
346
  props: {
342
347
  pageBreak?: ReportPageBreakEnum
343
348
  fit?: boolean
@@ -58,6 +58,7 @@ export interface IDndLayoutRow {
58
58
  }
59
59
  display?: IGridContainerConfig
60
60
  style?: ICssStyle
61
+ styleTemplateId?: string
61
62
  }
62
63
  export interface IDndLayoutCol {
63
64
  nodeType: DndLayoutNodeEnum.Col