form-craft-package 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -15,7 +15,7 @@ const useGetCurrentBreakpoint = (): DeviceBreakpointEnum => {
15
15
 
16
16
  if (!width) return DeviceBreakpointEnum.Default
17
17
 
18
- // if (width < parseInt(DEVICE_BREAKPOINTS[DeviceBreakpointEnum.Mobile])) return DeviceBreakpointEnum.Mobile
18
+ if (width < parseInt(DEVICE_BREAKPOINTS[DeviceBreakpointEnum.Mobile])) return DeviceBreakpointEnum.Mobile
19
19
 
20
20
  if (width < parseInt(DEVICE_BREAKPOINTS[DeviceBreakpointEnum.TabletPortrait]))
21
21
  return DeviceBreakpointEnum.TabletPortrait
@@ -1,10 +1,10 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react'
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import client from '../../../functions/axios-handler'
3
3
  import { parseJSON } from '../../../functions/forms/json-handlers'
4
4
  import FormDataListSkeleton_Table from '../../common/loading-skeletons/table'
5
- import { IFormData, IFormDataListConfig, IFormSchema, IDndLayoutStructure_Responsive } from '../../../types'
6
5
  import { ICustomFunctionCall } from '../layout-renderer/3-element/1-dynamic-button'
7
- import FormDataListTableComponent, { IReqDataConfig } from './table'
6
+ import FormDataListTableComponent from './table'
7
+ import { IFormDataListConfig, IFormSchema, IFormJoin, IDndLayoutElement, IFormDataListData } from '../../../types'
8
8
 
9
9
  function FormDataListComponent({
10
10
  formName,
@@ -15,14 +15,12 @@ function FormDataListComponent({
15
15
  onCustomFunctionCall,
16
16
  }: IFormDataListComponent) {
17
17
  const [loading, setLoading] = useState(true)
18
- const [formData, setFormData] = useState<{ data: IFormData[]; total: number }>({ data: [], total: 0 })
19
- const [dataListConfig, setDataListConfig] = useState<
20
- | {
21
- dataListConfig: IFormDataListConfig & { configForFormId: number }
22
- detailsLayoutConfig: IDndLayoutStructure_Responsive
23
- }
24
- | undefined
25
- >()
18
+ const [dataList, setDataList] = useState<{ data: IFormDataListData[]; total: number }>({ data: [], total: 0 })
19
+ const [listLayoutConfig, setListLayoutConfig] = useState<IDataListLayoutConfig>()
20
+
21
+ const dataProjectRef = useRef<{ [key: string]: string }>({})
22
+ const formJoinsRef = useRef<IFormJoin[]>([])
23
+ const defaultFilterReqDataRef = useRef<IDataListReqData | undefined>(undefined)
26
24
 
27
25
  const attachmentBaseUrl = useMemo(() => `${baseServerUrl}/api/attachment/${companyKey}`, [baseServerUrl, companyKey])
28
26
  const dataListHeaderContext = { formId, userId, attachmentBaseUrl, formName, companyKey }
@@ -33,50 +31,115 @@ function FormDataListComponent({
33
31
  if (res.status === 200) {
34
32
  const parsedData: IFormSchema | null = parseJSON(res.data.data)
35
33
  if (parsedData) {
34
+ console.log(parsedData.dataListConfig)
35
+ const { columns, joins, pagination, defaultFilter, defaultSorter } = parsedData.dataListConfig
36
+
37
+ if (Array.isArray(columns))
38
+ dataProjectRef.current = columns.reduce(
39
+ (curr, c) => (c.key ? { ...curr, [c.key.replace(/\./g, '_')]: `$${c.key}` } : curr),
40
+ {},
41
+ )
42
+ if (Array.isArray(joins)) formJoinsRef.current = joins
43
+
44
+ const defaultReqData: IDataListReqData = {}
45
+ if (!pagination?.hasNoPagination) {
46
+ defaultReqData.skip = 0
47
+ defaultReqData.size = pagination?.defaultPageSize ?? 10
48
+ }
49
+ if (defaultSorter) defaultReqData.sort = JSON.stringify({ [defaultSorter.field]: defaultSorter.order })
50
+ if (defaultFilter?.config)
51
+ // The operators are stored using @, and here it replaces @ with $. Otherwise, MongoDB's JSON-to-BSON conversion driver was detecting certain operators, especially $regex, and automatically modified them. To prevent data manipulation by the MongoDB driver, @ is used instead of $.
52
+ defaultReqData.match = JSON.stringify(defaultFilter.config).replaceAll('@', '$')
53
+
54
+ defaultFilterReqDataRef.current = defaultReqData
55
+
56
+ fetchFormDataList(defaultReqData)
57
+
36
58
  // configForFormId is passed to ensure that the table shows the correct data list when the forms are switched quickly
37
- setDataListConfig({
38
- dataListConfig: { ...parsedData.dataListConfig, configForFormId: formId },
39
- detailsLayoutConfig: {
40
- elements: parsedData.detailsConfig.elements,
41
- layouts: parsedData.detailsConfig.layouts,
42
- },
43
- })
59
+ setListLayoutConfig({ configForFormId: formId, dataListConfig: parsedData.dataListConfig })
44
60
  }
45
61
  }
46
62
  })
47
63
  }
48
64
  }, [formId])
49
65
 
50
- const fetchFormDataList = useCallback((reqData: IReqDataConfig, dynamicFormId?: number) => {
51
- if (!reqData || !dynamicFormId) return
66
+ const fetchFormDataList = useCallback(
67
+ (reqData?: IDataListReqData) => {
68
+ if (!reqData || !formId) return
52
69
 
53
- client
54
- .post(`/api/formdata/list/${dynamicFormId}`, {
55
- ...reqData,
56
- pagination: {
57
- number: reqData.pagination.current,
58
- size: reqData.pagination.pageSize,
59
- },
60
- })
61
- .then((res) => {
62
- if (res.status === 200)
63
- setFormData({
64
- ...res.data,
65
- data: res.data.data.map((d: IFormData) => {
66
- const { data, ...restD } = d
67
- return { ...restD, ...JSON.parse(data) }
68
- }),
69
- })
70
- })
71
- .finally(() => setLoading(false))
72
- }, [])
70
+ if (!reqData.skip && defaultFilterReqDataRef.current?.skip) reqData.skip = defaultFilterReqDataRef.current.skip
71
+ if (!reqData.size && defaultFilterReqDataRef.current?.size) reqData.size = defaultFilterReqDataRef.current.size
72
+ if (!reqData.sort && defaultFilterReqDataRef.current?.sort) reqData.sort = defaultFilterReqDataRef.current.sort
73
+ if (!reqData.match && defaultFilterReqDataRef.current?.match)
74
+ reqData.match = defaultFilterReqDataRef.current.match
75
+
76
+ console.log({ reqData })
77
+
78
+ client
79
+ .post(`/api/report/data/${formId}`, {
80
+ joins: formJoinsRef.current,
81
+ ...reqData,
82
+ project: JSON.stringify(dataProjectRef.current),
83
+ })
84
+ .then((res) => {
85
+ if (res.status === 200) setDataList({ data: res.data.data, total: res.data.totalRecords })
86
+ })
87
+ .finally(() => setLoading(false))
88
+ },
89
+ [formId],
90
+ )
91
+
92
+ // useEffect(() => {
93
+ // client.post('/api/report/data/86', {
94
+ // joins: [
95
+ // // {
96
+ // // formId: 85,
97
+ // // foreignField: '_id',
98
+ // // localField: 'Data.dId',
99
+ // // alias: '85',
100
+ // // },
101
+ // // {
102
+ // // formId: 84,
103
+ // // foreignField: '_id',
104
+ // // localField: '85.Data.cId',
105
+ // // alias: '84',
106
+ // // },
107
+ // // {
108
+ // // formId: 83,
109
+ // // foreignField: '_id',
110
+ // // localField: '84.Data.bId',
111
+ // // alias: '83',
112
+ // // },
113
+ // // {
114
+ // // formId: 82,
115
+ // // foreignField: '_id',
116
+ // // localField: '83.Data.aId',
117
+ // // alias: '82',
118
+ // // },
119
+ // ],
120
+ // // match: JSON.stringify({ $expr: { $eq: ['$82._id', { $toObjectId: '67b7c0a5e46d36ac56ea6548' }] } }),
121
+ // sort: JSON.stringify({
122
+ // '82.Data.field1': MongoDbSortOrderEnum.Descending,
123
+ // 'Data.field1': MongoDbSortOrderEnum.Ascending,
124
+ // }),
125
+ // project: JSON.stringify({
126
+ // Data_field1: '$Data.field1',
127
+ // '85_Data_field1': '$85.Data.field1',
128
+ // '84_Data_field1': '$84.Data.field1',
129
+ // '83_Data_field1': '$83.Data.field1',
130
+ // '82_Data_field1': '$82.Data.field1',
131
+ // }),
132
+ // skip: 0, // current
133
+ // limit: 10, // pageSize
134
+ // })
135
+ // }, [])
73
136
 
74
137
  return (
75
138
  <FormDataListTableComponent
76
- {...dataListConfig}
77
- dataList={formData}
139
+ layoutsConfigs={listLayoutConfig}
140
+ dataList={dataList}
78
141
  updateDataList={(reqData) => {
79
- fetchFormDataList(reqData, formId)
142
+ fetchFormDataList(reqData)
80
143
  }}
81
144
  parentLoading={loading}
82
145
  loadingBlock={<FormDataListSkeleton_Table />}
@@ -95,3 +158,17 @@ type IFormDataListComponent = {
95
158
  formId?: number
96
159
  userId: string | number
97
160
  } & ICustomFunctionCall
161
+
162
+ export interface IDataListReqData {
163
+ skip?: number
164
+ size?: number
165
+ sort?: string
166
+ match?: string
167
+ }
168
+
169
+ export type IDataListLayoutConfig =
170
+ | {
171
+ configForFormId: number
172
+ dataListConfig: IFormDataListConfig
173
+ }
174
+ | undefined
@@ -4,6 +4,7 @@ import {
4
4
  IDndLayoutStructure_Responsive,
5
5
  IFormData,
6
6
  IFormDataListConfig,
7
+ IFormDataListData,
7
8
  IFormDataTableColumn,
8
9
  } from '../../../types'
9
10
  import FormDataListHeaderComponent from './table-header'
@@ -20,10 +21,10 @@ import {
20
21
  FormDataListViewTypeEnum,
21
22
  MongoDbSortOrderEnum,
22
23
  } from '../../../enums'
24
+ import { IDataListReqData, IDataListLayoutConfig } from '.'
23
25
 
24
26
  export default function FormDataListTableComponent({
25
- dataListConfig,
26
- detailsLayoutConfig,
27
+ layoutsConfigs,
27
28
  parentLoading,
28
29
  dataList,
29
30
  loadingBlock,
@@ -63,16 +64,15 @@ export default function FormDataListTableComponent({
63
64
  setIsHandlingSchema(true)
64
65
  }, [location.pathname])
65
66
 
66
- useEffect(() => {
67
- if (debouncedFilterReqData) updateDataList(debouncedFilterReqData)
68
- }, [debouncedFilterReqData])
67
+ // useEffect(() => {
68
+ // if (debouncedFilterReqData) updateDataList(debouncedFilterReqData)
69
+ // }, [debouncedFilterReqData])
69
70
 
70
71
  const handleDataListConfig = useCallback(
71
- async (
72
- config: IFormDataListConfig & { configForFormId?: number },
73
- layoutConfig?: IDndLayoutStructure_Responsive,
74
- ) => {
75
- if (config.configForFormId && config.configForFormId !== formId) return
72
+ async (layoutsConfigs: IDataListLayoutConfig) => {
73
+ const { configForFormId, dataListConfig } = layoutsConfigs!
74
+
75
+ if (configForFormId !== formId) return
76
76
 
77
77
  const {
78
78
  columns = [],
@@ -81,42 +81,44 @@ export default function FormDataListTableComponent({
81
81
  defaultFilter,
82
82
  defaultSorter,
83
83
  ...restConfigs
84
- } = config
84
+ } = dataListConfig
85
85
 
86
- const elementsWithOptions_key = columns
87
- .filter((op) => op.isSelectOption)
88
- .map((op) => (op.key.includes('Data.') ? op.key.replace('Data.', '') : op.key))
86
+ console.log({ columns })
87
+
88
+ // TODO:
89
+ // const elementsWithOptions_key = columns.filter((op) => op.key && op.isSelectOption).map((op) => op.key)
89
90
 
90
91
  let optionKeyValuePair = {}
91
- if (elementsWithOptions_key.length > 0)
92
- optionKeyValuePair = await extractElementsOptions(layoutConfig?.elements ?? {}, elementsWithOptions_key)
92
+ // if (elementsWithOptions_key.length > 0)
93
+ // optionKeyValuePair = await extractElementsOptions(elements ?? {}, elementsWithOptions_key)
93
94
 
94
95
  setTableColumns(generateTableColumns(columns, optionKeyValuePair, dataListHeaderContext))
95
96
  setHeaderLayoutConfig({
96
97
  layouts: header.layouts,
97
- elements: appendOptionsFromDetailsLayout(header.elements, layoutConfig?.elements),
98
+ elements: header.elements,
99
+ // elements: appendOptionsFromDetailsLayout(header.elements, layoutConfig?.elements),
98
100
  })
99
101
  setOtherConfigs({ ...restConfigs, hasNoPagination: pagination?.hasNoPagination ?? false })
100
102
 
101
- if (defaultFilter && defaultFilter.type !== FilterConfigTypeEnum.NoFilter)
102
- defaultFilterRef.current = {
103
- ...defaultFilterRef.current,
104
- customFilter: defaultFilter.type === FilterConfigTypeEnum.ByAuthUser ? { createdById: userId } : {},
105
- dynamicFilter: defaultFilter.config
106
- ? // The operators are stored using @, and here it replaces @ with $. Otherwise, MongoDB's JSON-to-BSON conversion driver was detecting certain operators, especially $regex, and automatically modified them. To prevent data manipulation by the MongoDB driver, @ is used instead of $.
107
- JSON.stringify(defaultFilter.config).replaceAll('@', '$')
108
- : JSON.stringify({}),
109
- }
110
- defaultFilterRef.current = {
111
- ...defaultFilterRef.current,
112
- sort: defaultSorter ? { [defaultSorter.field]: defaultSorter.order } : {},
113
- pagination: {
114
- current: 1,
115
- pageSize: pagination?.defaultPageSize ?? 10,
116
- showSizeChanger: pagination?.showSizeChanger ?? false,
117
- pageSizeOptions: pagination?.pageSizeOptions ?? [],
118
- },
119
- }
103
+ // if (defaultFilter && defaultFilter.type !== FilterConfigTypeEnum.NoFilter)
104
+ // defaultFilterRef.current = {
105
+ // ...defaultFilterRef.current,
106
+ // customFilter: defaultFilter.type === FilterConfigTypeEnum.ByAuthUser ? { createdById: userId } : {},
107
+ // dynamicFilter: defaultFilter.config
108
+ // ? // The operators are stored using @, and here it replaces @ with $. Otherwise, MongoDB's JSON-to-BSON conversion driver was detecting certain operators, especially $regex, and automatically modified them. To prevent data manipulation by the MongoDB driver, @ is used instead of $.
109
+ // JSON.stringify(defaultFilter.config).replaceAll('@', '$')
110
+ // : JSON.stringify({}),
111
+ // }
112
+ // defaultFilterRef.current = {
113
+ // ...defaultFilterRef.current,
114
+ // sort: defaultSorter ? { [defaultSorter.field]: defaultSorter.order } : {},
115
+ // pagination: {
116
+ // current: 1,
117
+ // pageSize: pagination?.defaultPageSize ?? 10,
118
+ // showSizeChanger: pagination?.showSizeChanger ?? false,
119
+ // pageSizeOptions: pagination?.pageSizeOptions ?? [],
120
+ // },
121
+ // }
120
122
 
121
123
  setIsHandlingSchema(false)
122
124
  isFinishedHandlingRef.current = true
@@ -125,8 +127,8 @@ export default function FormDataListTableComponent({
125
127
  )
126
128
 
127
129
  useEffect(() => {
128
- if (dataListConfig) handleDataListConfig(dataListConfig, detailsLayoutConfig)
129
- }, [dataListConfig, detailsLayoutConfig])
130
+ if (layoutsConfigs) handleDataListConfig(layoutsConfigs)
131
+ }, [layoutsConfigs])
130
132
 
131
133
  const tableColumnsFiltered = useMemo(
132
134
  () => tableColumns.filter((op) => !Array.isArray(op.hideScreens) || !op.hideScreens.includes(currentBreakpoint)),
@@ -156,39 +158,35 @@ export default function FormDataListTableComponent({
156
158
  onCustomFunctionCall={onCustomFunctionCall}
157
159
  {...dataListHeaderContext}
158
160
  />
159
- {dataListConfig?.listType === FormDataListViewTypeEnum.Table && (
160
- <Table<IFormData>
161
+ {(!otherConfigs || otherConfigs.listType === FormDataListViewTypeEnum.Table) && (
162
+ <Table<IFormDataListData>
161
163
  dataSource={dataList.data}
162
164
  columns={tableColumnsFiltered}
163
- rowKey={(record) => record.id}
164
- pagination={
165
- otherConfigs?.hasNoPagination
166
- ? false
167
- : { ...defaultFilterRef.current.pagination, ...filterReqData?.pagination, total: dataList.total }
168
- }
165
+ rowKey={(record) => record.id} // TODO: change this to _id
166
+ pagination={otherConfigs?.hasNoPagination ? false : { ...filterReqData?.pagination, total: dataList.total }}
169
167
  loading={parentLoading}
170
- onChange={(cbPagination, _, sorter: SorterResult<IFormData> | SorterResult<IFormData>[]) => {
171
- console.log('ON TABLE CHANGE', cbPagination, sorter)
172
- setParentLoading(true)
173
-
174
- setFilterReqData((c) => {
175
- if (!c) return c
176
-
177
- return {
178
- ...c,
179
- sort:
180
- sorter && (sorter as SorterResult<IFormData>).field
181
- ? {
182
- [(sorter as SorterResult<IFormData>).field as string]:
183
- (sorter as SorterResult<IFormData>).order === 'ascend'
184
- ? MongoDbSortOrderEnum.Ascending
185
- : MongoDbSortOrderEnum.Descending,
186
- }
187
- : {},
188
- pagination: cbPagination as IPagination,
189
- }
190
- })
191
- }}
168
+ // onChange={(cbPagination, _, sorter: SorterResult<IFormData> | SorterResult<IFormData>[]) => {
169
+ // console.log('ON TABLE CHANGE', cbPagination, sorter)
170
+ // setParentLoading(true)
171
+
172
+ // setFilterReqData((c) => {
173
+ // if (!c) return c
174
+
175
+ // return {
176
+ // ...c,
177
+ // sort:
178
+ // sorter && (sorter as SorterResult<IFormData>).field
179
+ // ? {
180
+ // [(sorter as SorterResult<IFormData>).field as string]:
181
+ // (sorter as SorterResult<IFormData>).order === 'ascend'
182
+ // ? MongoDbSortOrderEnum.Ascending
183
+ // : MongoDbSortOrderEnum.Descending,
184
+ // }
185
+ // : {},
186
+ // pagination: cbPagination as IPagination,
187
+ // }
188
+ // })
189
+ // }}
192
190
  />
193
191
  )}
194
192
  </>
@@ -196,13 +194,12 @@ export default function FormDataListTableComponent({
196
194
  }
197
195
 
198
196
  type IFormDataListTableComponent = {
199
- dataListConfig?: IFormDataListConfig & { configForFormId?: number }
200
- detailsLayoutConfig?: IDndLayoutStructure_Responsive
201
- dataList: { data: IFormData[]; total: number }
197
+ layoutsConfigs: IDataListLayoutConfig
198
+ dataList: { data: IFormDataListData[]; total: number }
202
199
  parentLoading: boolean
203
200
  loadingBlock?: JSX.Element
204
201
  setParentLoading: React.Dispatch<React.SetStateAction<boolean>>
205
- updateDataList: (reqData: IReqDataConfig) => void
202
+ updateDataList: (reqData: IDataListReqData) => void
206
203
  } & ICustomFunctionCall &
207
204
  IDataListHeaderContext
208
205
 
@@ -4,63 +4,56 @@ import LayoutRendererCol from '../2-col'
4
4
  import { memo, ReactElement, ReactNode, useMemo, useState } from 'react'
5
5
  import { LayoutRowConditionalHeaderRenderer } from './header-render'
6
6
  import { LayoutRowRepeatableRenderer } from './repeatable-render'
7
- import {
8
- IButtonElementProps,
9
- IDndLayoutElement,
10
- IFormLayoutElementConditions,
11
- IDndLayoutRow,
12
- } from '../../../../types'
7
+ import { IButtonElementProps, IDndLayoutElement, IFormLayoutElementConditions, IDndLayoutRow } from '../../../../types'
13
8
 
14
9
  export const LayoutRendererRow = memo(
15
10
  ({ basePath = [], rowData, titleComponent, formContext, elements, renderButton }: ILayoutRendererRow) => {
16
11
  const [hiddenElementCount, setHiddenElementCount] = useState(0)
17
12
 
18
13
  const styleConfig = useMemo(() => {
19
- if (!rowData.style || !rowData.style) return {}
14
+ if (!rowData.style) return {}
20
15
  return kebabCaseToCamelCase(rowData.style)
21
16
  }, [rowData.style])
22
17
 
23
18
  return (
24
- <>
25
- <LayoutRowConditionalHeaderRenderer header={rowData.props?.header}>
26
- <LayoutRowRepeatableRenderer basePath={basePath} repeatingSection={rowData.props?.repeatingSection}>
27
- {(formListItemProps) => {
28
- const style: { [key: string]: any } = {
29
- ...styleConfig,
30
- ...getFlexContainerStyle(rowData.display),
31
- }
32
- if (hiddenElementCount === rowData.children.length) style.display = 'none'
19
+ <LayoutRowConditionalHeaderRenderer header={rowData.props?.header}>
20
+ <LayoutRowRepeatableRenderer basePath={basePath} repeatingSection={rowData.props?.repeatingSection}>
21
+ {(formListItemProps) => {
22
+ const style: { [key: string]: any } = {
23
+ ...styleConfig,
24
+ ...getFlexContainerStyle(rowData.display),
25
+ }
26
+ if (hiddenElementCount === rowData.children.length) style.display = 'none'
33
27
 
34
- return (
35
- <div style={style}>
36
- {rowData.children.map((col, colIdx) => (
37
- <LayoutRendererCol
38
- key={colIdx}
39
- basePath={formListItemProps ? formListItemProps.updatedBasePath : basePath}
40
- colData={col}
41
- titleComponent={titleComponent}
42
- elements={elements}
43
- formContext={formContext}
44
- colStyle={getColumnStyle(colIdx, rowData.display)}
45
- renderButton={renderButton}
46
- hideRow={(isHidden) =>
47
- setHiddenElementCount((c) => {
48
- if (isHidden) return c + 1
49
- else {
50
- if (c - 1 < 0) return 0
51
- return c - 1
52
- }
53
- })
54
- }
55
- />
56
- ))}
57
- {formListItemProps?.removeButton}
58
- </div>
59
- )
60
- }}
61
- </LayoutRowRepeatableRenderer>
62
- </LayoutRowConditionalHeaderRenderer>
63
- </>
28
+ return (
29
+ <div style={{ ...style, maxWidth: '100vw', overflowX: 'auto' }}>
30
+ {rowData.children.map((col, colIdx) => (
31
+ <LayoutRendererCol
32
+ key={colIdx}
33
+ basePath={formListItemProps ? formListItemProps.updatedBasePath : basePath}
34
+ colData={col}
35
+ titleComponent={titleComponent}
36
+ elements={elements}
37
+ formContext={formContext}
38
+ colStyle={getColumnStyle(colIdx, rowData.display)}
39
+ renderButton={renderButton}
40
+ hideRow={(isHidden) =>
41
+ setHiddenElementCount((c) => {
42
+ if (isHidden) return c + 1
43
+ else {
44
+ if (c - 1 < 0) return 0
45
+ return c - 1
46
+ }
47
+ })
48
+ }
49
+ />
50
+ ))}
51
+ {formListItemProps?.removeButton}
52
+ </div>
53
+ )
54
+ }}
55
+ </LayoutRowRepeatableRenderer>
56
+ </LayoutRowConditionalHeaderRenderer>
64
57
  )
65
58
  },
66
59
  )
package/src/constants.ts CHANGED
@@ -110,4 +110,23 @@ export const DEFAULT_CONFIG = {
110
110
  siteIdentity: { logoUrl: '/favicon.png', iconUrl: '/favicon.png', title: 'Site Title' },
111
111
  siteMenus: [],
112
112
  siteLanguages: [{ value: 'en', label: 'English', defaultSelected: true }],
113
+ loginLayout: {
114
+ layout: 'center',
115
+ backgroundType: 'color',
116
+ background: '#f5f5f5',
117
+ backgroundImage: '',
118
+ formStyle: 'mini',
119
+ logoPosition: 'top',
120
+ form: {
121
+ title: 'Login',
122
+ color: '#245780',
123
+ background: '#ffffff',
124
+ size: 20,
125
+ width: 400,
126
+ weight: 700,
127
+ padding: 20,
128
+ spacing: 20,
129
+ fields: ['email', 'password', 'forgot-password']
130
+ }
131
+ }
113
132
  }
@@ -2,5 +2,6 @@ export enum AppearanceMenu {
2
2
  siteIdentity = 'Site Identity',
3
3
  siteLayouts = 'Site Layouts',
4
4
  siteMenus = 'Site Menus',
5
- siteLanguages = 'Site Languages'
6
- }
5
+ siteLanguages = 'Site Languages',
6
+ loginLayout = 'Login Layout'
7
+ }
@@ -8,6 +8,7 @@ export enum ElementTypeEnum {
8
8
  NumberInput = 'NumberInput',
9
9
  CurrencyInput = 'CurrencyInput',
10
10
  Select = 'Select',
11
+ TreeSelect = 'TreeSelect',
11
12
  Radio = 'Radio',
12
13
  Checkbox = 'Checkbox',
13
14
  CheckboxGroup = 'CheckboxGroup',
@@ -94,11 +94,11 @@ export const appendOptionsFromDetailsLayout = (
94
94
  [ElementTypeEnum.Select, ElementTypeEnum.Radio].includes(element.elementType) &&
95
95
  element &&
96
96
  (element as ISelectElement | IRadioElement).props?.optionSource?.type ===
97
- FieldElementOptionSourceEnum.ReadFromDetails
97
+ FieldElementOptionSourceEnum.ReadFromDetails
98
98
  ) {
99
99
  const detailsElement =
100
100
  detailsLayoutElements[
101
- ((element as ISelectElement | IRadioElement).props.optionSource as IOptionSourceReadFromDetails)?.field ?? ''
101
+ ((element as ISelectElement | IRadioElement).props.optionSource as IOptionSourceReadFromDetails)?.field ?? ''
102
102
  ]
103
103
  updatedElements[elementKey] = {
104
104
  ...element,
@@ -108,7 +108,7 @@ export const appendOptionsFromDetailsLayout = (
108
108
  type: FieldElementOptionSourceEnum.Static,
109
109
  options: detailsElement
110
110
  ? ((detailsElement as ISelectElement | IRadioElement).props.optionSource as IOptionSourceConstant)
111
- ?.options ?? []
111
+ ?.options ?? []
112
112
  : [],
113
113
  },
114
114
  },
@@ -139,7 +139,7 @@ export const getColumnStyle = (itemIdx: number, display?: IGridContainerConfig):
139
139
  /** --------------------------------------------------------------------------------------------------------- */
140
140
 
141
141
  export const constructDynamicFormHref = (name: string) => '/d/' + name.split(' ').join('-').toLowerCase()
142
-
142
+ export const constructDynamicReportHref = (name: string) => '/d/reports/' + name.split(' ').join('-').toLowerCase()
143
143
  /** --------------------------------------------------------------------------------------------------------- */
144
144
 
145
145
  export const extractDynamicFormHref = (pathname: string = '') => {
@@ -1,6 +1,7 @@
1
1
  export interface ICompanyViewModel {
2
2
  id: number;
3
3
  name?: string | null;
4
+ domain?: string;
4
5
  blobSAS?: string | null;
5
6
  defaultDataId?: string | null;
6
7
  key?: string | null;
@@ -15,7 +16,29 @@ export interface ICompanyConfig {
15
16
  siteLayout: ISiteLayout;
16
17
  siteMenus: ISiteMenus;
17
18
  siteLanguages: ISiteLanguages;
19
+ loginLayout: ILoginLayout;
20
+ }
21
+ export interface ILoginLayout {
22
+ layout: string;
23
+ backgroundType: string;
24
+ background: string;
25
+ backgroundImage: string;
26
+ formStyle: string;
27
+ logoPosition: string;
28
+ form: ILoginFormLayout;
29
+ }
30
+ export interface ILoginFormLayout {
31
+ width: number;
32
+ padding: number;
33
+ title: string;
34
+ background: string;
35
+ size: number;
36
+ weight: number;
37
+ color: string;
38
+ spacing: number;
39
+ fields: string[];
18
40
  }
41
+
19
42
  export type ISiteLanguages = ISiteLanguageItem[]
20
43
  export interface ISiteLanguageItem {
21
44
  value: string;
@@ -25,26 +48,37 @@ export interface ISiteLanguageItem {
25
48
  export type ISiteMenus = ISiteMenuItem[]
26
49
 
27
50
  export interface ISiteMenuItem {
28
- id: number
29
- name: string
30
- publicAccess?: boolean
31
- icon?: string
32
- customLink?: string
51
+ id: number;
52
+ name: string;
53
+ displayName?: string;
54
+ publicAccess?: boolean;
55
+ icon?: string;
56
+ customLink?: string;
33
57
  children?: ISiteMenuItem[]
34
58
  }
59
+ export interface IProfileMenuItem {
60
+ id: number;
61
+ name: string;
62
+ displayName?: string;
63
+ customLink?: string;
64
+ icon?: string;
65
+ onClick?: () => void
66
+ }
35
67
  export interface IExtendedSiteMenuItem extends ISiteMenuItem {
36
68
  isMenuGroup?: boolean;
37
69
  isReportGroup?: boolean;
70
+ menuType?: string;
38
71
  }
39
72
  export interface MenuBuilderProps {
40
- reportItems: ISiteReportItem[]
41
- menuItems: ISiteMenuItem[]
42
- onChange?: (menu: ISiteMenuItem[]) => void
43
- initialValue: ISiteMenuItem[]
73
+ reportItems: ISiteReportItem[];
74
+ menuItems: ISiteMenuItem[];
75
+ onChange?: (menu: ISiteMenuItem[]) => void;
76
+ initialValue: ISiteMenuItem[];
44
77
  }
45
78
  export interface ISiteReportItem {
46
79
  id: number;
47
80
  name: string;
81
+ displayName: string;
48
82
  description: string;
49
83
  isActive: boolean;
50
84
  order: number;
@@ -130,4 +164,5 @@ export interface FontSelectorProps {
130
164
  initialFont?: string;
131
165
  }
132
166
 
133
- export * from './site-layout/authenticated';
167
+ export * from './site-layout/authenticated';
168
+ export * from './site-layout/unauthenticated';
@@ -2,7 +2,7 @@ import { FaCaretDown, FaTimes, FaUser } from 'react-icons/fa'
2
2
  import { HiMenu } from 'react-icons/hi'
3
3
  import { ICompanyConfig, ILayoutTemplateProps, IExtendedSiteMenuItem } from '../..'
4
4
  import { Link } from 'react-router-dom'
5
- import { constructDynamicFormHref, isEncodedURI } from '../../../../functions/forms'
5
+ import { constructDynamicFormHref, constructDynamicReportHref, isEncodedURI } from '../../../../functions/forms'
6
6
  import { useEffect, useState } from 'react'
7
7
  import { Button, Drawer, Dropdown, Layout, Menu } from 'antd'
8
8
  import * as FaIcons from 'react-icons/fa'
@@ -11,7 +11,7 @@ const { Header, Content, Sider } = Layout
11
11
  export const layoutTemplates = [
12
12
  {
13
13
  name: 'template_1',
14
- value: ({ children, userName, menuItems, config, logoUrl, isPreview }: ILayoutTemplateProps) => {
14
+ value: ({ children, menuItems, config, logoUrl, isPreview }: ILayoutTemplateProps) => {
15
15
  const [sidebarOpen, setSidebarOpen] = useState(false)
16
16
  const siteConfigs = config?.siteLayout?.siteConfigs
17
17
  const navigationWidth = siteConfigs?.custom?.navigationWidth || 200
@@ -28,12 +28,17 @@ export const layoutTemplates = [
28
28
  }, [config])
29
29
 
30
30
  const renderMenuItem = (form: IExtendedSiteMenuItem, level = 0) => {
31
- let href =
32
- form.customLink === '' || form.isMenuGroup || form.isReportGroup
33
- ? '#'
34
- : form.customLink
35
- ? form.customLink
36
- : constructDynamicFormHref(form.name)
31
+ let href = ''
32
+
33
+ if (form.customLink === '' || form.isMenuGroup || form.isReportGroup) {
34
+ href = '#'
35
+ } else if (form.customLink) {
36
+ href = form.customLink
37
+ } else if (form.menuType && form.menuType === 'report') {
38
+ href = constructDynamicReportHref(form.name)
39
+ } else {
40
+ href = constructDynamicFormHref(form.name)
41
+ }
37
42
  let isParentMenu = href === '#' && form.children && form.children.length > 0
38
43
  const IconComponent = form.icon ? (FaIcons as any)[form.icon] : null
39
44
  const isExpanded = expandedMenus[form.id]
@@ -81,7 +86,7 @@ export const layoutTemplates = [
81
86
  <img alt="" src={form.icon ?? ''} className="w-4 h-4 object-contain" />
82
87
  )}
83
88
  </div>
84
- <span className="flex-grow">{form.name}</span>
89
+ <span className="flex-grow">{form.displayName || form.name}</span>
85
90
  {isParentMenu &&
86
91
  (isExpanded ? <FaIcons.FaAngleUp className="w-4 h-4" /> : <FaIcons.FaAngleDown className="w-4 h-4" />)}
87
92
  </Link>
@@ -108,7 +113,7 @@ export const layoutTemplates = [
108
113
  )
109
114
  return (
110
115
  <Layout className="h-screen overflow-y-auto overflow-x-hidden">
111
- <Header className="sticky top-0 z-40 flex items-center shadow-sm justify-between px-1">
116
+ <Header className="sticky top-0 z-40 flex items-center shadow-sm justify-between px-1 overflow-hidden">
112
117
  {/* Logo */}
113
118
  <div className="flex">
114
119
  <a href="/">
@@ -217,30 +222,33 @@ export const layoutTemplates = [
217
222
  },
218
223
  {
219
224
  name: 'template_2',
220
- value: ({ children, userName, menuItems, config, logoUrl, isPreview }: ILayoutTemplateProps) => {
225
+ value: ({ children, menuItems, config, logoUrl, isPreview }: ILayoutTemplateProps) => {
221
226
  const [sidebarOpen, setSidebarOpen] = useState(false)
222
227
  const siteConfigs = config?.siteLayout?.siteConfigs
223
228
  const contentPadding = siteConfigs?.custom?.contentPadding || 20
224
- const [expandedMenus, setExpandedMenus] = useState<{ [key: number]: boolean }>({})
229
+
225
230
  const renderMenuItem = (form: IExtendedSiteMenuItem, level = 0) => {
226
- let href =
227
- form.customLink === '' || form.isMenuGroup || form.isReportGroup
228
- ? '#'
229
- : form.customLink
230
- ? form.customLink
231
- : constructDynamicFormHref(form.name)
231
+ let href = ''
232
+
233
+ if (form.customLink === '' || form.isMenuGroup || form.isReportGroup) {
234
+ href = '#'
235
+ } else if (form.customLink) {
236
+ href = form.customLink
237
+ } else if (form.menuType && form.menuType === 'report') {
238
+ href = constructDynamicReportHref(form.name)
239
+ } else {
240
+ href = constructDynamicFormHref(form.name)
241
+ }
232
242
  let isParentMenu = href === '#' && form.children && form.children.length > 0
233
- const isExpanded = expandedMenus[form.id]
243
+
234
244
  const IconComponent = form.icon ? (FaIcons as any)[form.icon] : null
235
- const toggleMenu = (id: number) => {
236
- setExpandedMenus((prev) => ({ ...prev, [id]: !prev[id] }))
237
- }
245
+
238
246
  return (
239
247
  <div key={form.id} className="relative group">
240
248
  <Link
241
249
  to={href}
242
250
  target={form.customLink && '_blank'}
243
- onClick={isParentMenu ? () => toggleMenu(form.id) : undefined}
251
+ onClick={isParentMenu ? (e) => e.preventDefault() : undefined}
244
252
  style={{
245
253
  borderRadius: `${siteConfigs?.Link.borderRadius}px`,
246
254
  backgroundColor: isActive(window.location.pathname, href)
@@ -274,7 +282,7 @@ export const layoutTemplates = [
274
282
  <img alt="" src={form.icon ?? ''} className="w-4 h-4 object-contain" />
275
283
  )}
276
284
  </div>
277
- <span className="flex-grow">{form.name}</span>
285
+ <span className="flex-grow">{form.displayName || form.name}</span>
278
286
  </Link>
279
287
  {isParentMenu && (
280
288
  <div className="absolute left-0 top-full hidden group-hover:flex flex-col bg-white shadow-md rounded-md z-50 min-w-[180px]">
@@ -394,7 +402,7 @@ export const layoutTemplates = [
394
402
  },
395
403
  {
396
404
  name: 'template_3',
397
- value: ({ children, userName, menuItems, logoUrl, config, isPreview }: ILayoutTemplateProps) => {
405
+ value: ({ children, menuItems, logoUrl, config, isPreview }: ILayoutTemplateProps) => {
398
406
  const [sidebarOpen, setSidebarOpen] = useState(false)
399
407
  const siteConfigs = config?.siteLayout?.siteConfigs
400
408
  const navigationWidth = siteConfigs?.custom?.navigationWidth || 200
@@ -411,12 +419,17 @@ export const layoutTemplates = [
411
419
  setExpandedMenus(initialExpandedStates)
412
420
  }, [config])
413
421
  const renderMenuItem = (form: IExtendedSiteMenuItem, level = 0) => {
414
- let href =
415
- form.customLink === '' || form.isMenuGroup || form.isReportGroup
416
- ? '#'
417
- : form.customLink
418
- ? form.customLink
419
- : constructDynamicFormHref(form.name)
422
+ let href = ''
423
+
424
+ if (form.customLink === '' || form.isMenuGroup || form.isReportGroup) {
425
+ href = '#'
426
+ } else if (form.customLink) {
427
+ href = form.customLink
428
+ } else if (form.menuType && form.menuType === 'report') {
429
+ href = constructDynamicReportHref(form.name)
430
+ } else {
431
+ href = constructDynamicFormHref(form.name)
432
+ }
420
433
  let isParentMenu = href === '#' && form.children && form.children.length > 0
421
434
  const IconComponent = form.icon ? (FaIcons as any)[form.icon] : null
422
435
  const isExpanded = expandedMenus[form.id]
@@ -464,7 +477,7 @@ export const layoutTemplates = [
464
477
  <img alt="" src={form.icon ?? ''} className="w-4 h-4 object-contain" />
465
478
  )}
466
479
  </div>
467
- <span className="flex-grow">{form.name}</span>
480
+ <span className="flex-grow">{form.displayName || form.name}</span>
468
481
  {isParentMenu &&
469
482
  (isExpanded ? <FaIcons.FaAngleUp className="w-4 h-4" /> : <FaIcons.FaAngleDown className="w-4 h-4" />)}
470
483
  </Link>
@@ -10,3 +10,28 @@ export const unauthorizedLayoutTemplates = [
10
10
  ),
11
11
  },
12
12
  ]
13
+ export const layoutOptions = [
14
+ { label: 'Centered', value: 'center' },
15
+ { label: 'Image Left / Form Right', value: 'split-left' },
16
+ { label: 'Form Left / Image Right', value: 'split-right' },
17
+ ]
18
+ export const logoOptions = [
19
+ { label: 'Top', value: 'top' },
20
+ { label: 'Bottom', value: 'bottom' },
21
+ { label: 'None', value: 'none' },
22
+ ]
23
+ export const formStyleOptions = [
24
+ { label: 'Full', value: 'full' },
25
+ { label: 'Mini', value: 'mini' },
26
+ ]
27
+ export const backgroundTypeOptions = [
28
+ { label: 'Color', value: 'color' },
29
+ { label: 'Image', value: 'image' },
30
+ ]
31
+
32
+ export const formFieldOptions = [
33
+ { label: 'Username', value: 'username' },
34
+ { label: 'Email', value: 'email' },
35
+ { label: 'Password', value: 'password' },
36
+ { label: 'Forgot Password', value: 'forgot-password' },
37
+ ]
@@ -20,6 +20,7 @@ export interface IFormDataListConfig {
20
20
  listType: FormDataListViewTypeEnum
21
21
  columns: IFormDataListColumn[]
22
22
  header: IDndLayoutStructure_Responsive
23
+ joins?: IFormJoin[]
23
24
  pagination?: IFormDataListPagination
24
25
  title?: string
25
26
  showCount?: boolean
@@ -53,3 +54,6 @@ export interface IFormJoin {
53
54
  localField: string
54
55
  alias: string
55
56
  }
57
+ export interface IFormDataListData {
58
+ [key: string]: any
59
+ }