form-craft-package 1.11.5-dev.0 → 1.11.6-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.5-dev.0",
3
+ "version": "1.11.6-dev.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -53,9 +53,14 @@ function FormDataListComponentChild({
53
53
  () => `${baseServerUrl}/api/attachment/${companyKey}`,
54
54
  [baseServerUrl, companyKey],
55
55
  )
56
- const headerLayoutContext = { formId, attachmentBaseUrl, companyKey, onCustomFunctionCall }
57
-
58
56
  const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(formId)
57
+ const headerLayoutContext = {
58
+ formId,
59
+ attachmentBaseUrl,
60
+ companyKey,
61
+ onCustomFunctionCall,
62
+ formRelationships: cachedConfig?.relationships,
63
+ }
59
64
 
60
65
  useEffect(() => {
61
66
  reportDataApiCancelFuncRef.current?.()
@@ -116,6 +121,8 @@ function FormDataListComponentChild({
116
121
  const cacheKey = buildDataListCacheKey('form-data-list', [
117
122
  formId,
118
123
  nextReqData.match ?? '',
124
+ nextReqData.pipeline ?? '',
125
+ nextReqData.joins ?? [],
119
126
  nextReqData.sort ?? '',
120
127
  nextReqData.skip ?? 0,
121
128
  nextReqData.limit ?? '',
@@ -133,13 +140,49 @@ function FormDataListComponentChild({
133
140
  setCheckingForUpdates(true)
134
141
  apiCallCounterRef.current += 1
135
142
 
136
- const { current, ...restReqData } = nextReqData
137
-
138
- const { request, cancel } = cancelableClient.post(`/api/report/data/${formId}`, {
139
- joins: formJoinsRef.current,
140
- project: JSON.stringify(dataProjectRef.current),
141
- ...restReqData,
142
- })
143
+ const { current, joins, pipeline, ...restReqData } = nextReqData
144
+
145
+ const resolvedJoins = mergeJoins([formJoinsRef.current, joins || []])
146
+ const { request, cancel } = pipeline
147
+ ? cancelableClient.post(`/api/report/dataDynamic/${formId}`, {
148
+ joins: resolvedJoins,
149
+ pipeline: JSON.stringify([
150
+ ...(JSON.parse(pipeline) as { [key: string]: any }[]),
151
+ ...(restReqData.sort
152
+ ? [
153
+ {
154
+ $sort: Object.entries(JSON.parse(restReqData.sort)).reduce(
155
+ (curr, [field, value]) => ({ ...curr, [`doc.${field}`]: value }),
156
+ {},
157
+ ),
158
+ },
159
+ ]
160
+ : []),
161
+ ...(Object.values(dataProjectRef.current).length
162
+ ? [
163
+ {
164
+ $project: Object.entries(dataProjectRef.current).reduce(
165
+ (curr, [key, value]) => ({
166
+ ...curr,
167
+ [key]:
168
+ typeof value === 'string' && value.startsWith('$')
169
+ ? `$doc.${value.slice(1)}`
170
+ : value,
171
+ }),
172
+ {},
173
+ ),
174
+ },
175
+ ]
176
+ : []),
177
+ ]),
178
+ skip: restReqData.skip,
179
+ limit: restReqData.limit,
180
+ })
181
+ : cancelableClient.post(`/api/report/data/${formId}`, {
182
+ joins: resolvedJoins,
183
+ project: JSON.stringify(dataProjectRef.current),
184
+ ...restReqData,
185
+ })
143
186
  reportDataApiCancelFuncRef.current = cancel
144
187
 
145
188
  request
@@ -150,6 +193,8 @@ function FormDataListComponentChild({
150
193
  setCachedDataList(cacheKey, payload, {
151
194
  formId,
152
195
  match: nextReqData.match,
196
+ pipeline: nextReqData.pipeline,
197
+ joins: nextReqData.joins,
153
198
  sort: nextReqData.sort,
154
199
  skip: nextReqData.skip,
155
200
  limit: nextReqData.limit,
@@ -250,6 +295,8 @@ function FormDataListComponentChild({
250
295
  const initialCacheKey = buildDataListCacheKey('form-data-list', [
251
296
  formId,
252
297
  initialReqData.match ?? '',
298
+ initialReqData.pipeline ?? '',
299
+ initialReqData.joins ?? [],
253
300
  initialReqData.sort ?? '',
254
301
  initialSkip ?? 0,
255
302
  initialReqData.limit ?? '',
@@ -304,4 +351,6 @@ export interface IDataListReqData {
304
351
  limit?: number
305
352
  sort?: string
306
353
  match?: string
354
+ pipeline?: string
355
+ joins?: IFormJoin[]
307
356
  }
@@ -1,8 +1,8 @@
1
1
  import { Form } from 'antd'
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
3
  import dayjs from 'dayjs'
4
- import { extractFiltersFromLayout } from '../../../functions/forms'
5
- import { DeviceBreakpointEnum, FilterConfigTypeEnum } from '../../../enums'
4
+ import { extractFiltersFromLayout, mergeJoins } from '../../../functions/forms'
5
+ import { DeviceBreakpointEnum, FilterConfigTypeEnum, FormRelationshipEnum } from '../../../enums'
6
6
  import { LayoutRendererRow } from '../layout-renderer/1-row'
7
7
  import useGetCurrentBreakpoint from '../../common/custom-hooks/use-window-width.hook'
8
8
  import { IDataListHeaderLayoutContext } from './table'
@@ -23,6 +23,8 @@ import {
23
23
  IFilterCustom,
24
24
  IFilterNested,
25
25
  IFilterSimple,
26
+ IFormJoin,
27
+ IFormRelationshipConfig,
26
28
  } from '../../../types'
27
29
 
28
30
  export default function FormDataListHeaderComponent({
@@ -62,20 +64,35 @@ export default function FormDataListHeaderComponent({
62
64
 
63
65
  if (filtersWithConfig.length > 0) {
64
66
  const filtersToApply = []
67
+ const joinsToApply: IFormJoin[][] = []
68
+ let pipelineToApply: string | undefined
69
+
65
70
  for (const filterConfig of filtersWithConfig) {
66
71
  const [field, value] = filterConfig
67
72
  const config = filterConfigs[field]
68
73
 
69
74
  if ('type' in config) {
70
- const stringifiedFilter = await handleFilterValues(config as IFilterConfig, value)
71
- if (stringifiedFilter) filtersToApply.push(stringifiedFilter)
75
+ const filterData = await handleFilterValues(
76
+ config as IFilterConfig,
77
+ value,
78
+ headerLayoutContext.formRelationships,
79
+ )
80
+ if (filterData?.match) filtersToApply.push(filterData.match)
81
+ if (filterData?.pipeline && !pipelineToApply) pipelineToApply = filterData.pipeline
82
+ if (Array.isArray(filterData?.joins) && filterData.joins.length) joinsToApply.push(filterData.joins)
72
83
  } else {
73
84
  // cases like radio buttons, where each has its own filter, but only 1 needs to apply
74
85
  const nestedConfig = config as IFilterSimple
75
86
  const eachConfig = nestedConfig[value as string]
76
87
 
77
- const stringifiedFilter = await handleFilterValues(eachConfig as IFilterConfig, value)
78
- if (stringifiedFilter) filtersToApply.push(stringifiedFilter)
88
+ const filterData = await handleFilterValues(
89
+ eachConfig as IFilterConfig,
90
+ value,
91
+ headerLayoutContext.formRelationships,
92
+ )
93
+ if (filterData?.match) filtersToApply.push(filterData.match)
94
+ if (filterData?.pipeline && !pipelineToApply) pipelineToApply = filterData.pipeline
95
+ if (Array.isArray(filterData?.joins) && filterData.joins.length) joinsToApply.push(filterData.joins)
79
96
  }
80
97
  }
81
98
 
@@ -85,13 +102,21 @@ export default function FormDataListHeaderComponent({
85
102
  return { ...curr, ...parsedDFilter }
86
103
  }, {})
87
104
 
88
- const updatedDynamicFilter = JSON.stringify(
89
- // dynamicFilters.length === 1 ? dynamicFilters[0] : { $and: dynamicFilters },
90
- dynamicFilters,
91
- )
105
+ const match = Object.values(dynamicFilters).length ? JSON.stringify(dynamicFilters) : undefined
106
+
107
+ if (pipelineToApply) {
108
+ const parsedPipeline = JSON.parse(pipelineToApply) as { $match?: { [key: string]: any } }[]
109
+ const pipeline = JSON.stringify([
110
+ ...(match ? [{ $match: JSON.parse(match) }] : []),
111
+ ...(parsedPipeline[0]?.$match ? parsedPipeline.slice(1) : parsedPipeline),
112
+ ])
113
+
114
+ updateDynamicFilter({ match, pipeline, joins: mergeJoins(joinsToApply) })
115
+ return
116
+ }
92
117
 
93
- updateDynamicFilter(updatedDynamicFilter)
94
- } else updateDynamicFilter()
118
+ updateDynamicFilter({ match })
119
+ } else updateDynamicFilter({})
95
120
  },
96
121
  [filterConfigs],
97
122
  )
@@ -158,12 +183,16 @@ export default function FormDataListHeaderComponent({
158
183
 
159
184
  type IFormDataListHeaderComponent = {
160
185
  layoutConfig?: IDndLayoutStructure_Responsive
161
- updateDynamicFilter: (match?: string) => void
186
+ updateDynamicFilter: (data?: { match?: string; pipeline?: string; joins?: IFormJoin[] }) => void
162
187
  dataCount?: 'pending' | number
163
188
  headerLayoutContext: IDataListHeaderLayoutContext
164
189
  }
165
190
 
166
- const handleFilterValues = async (config: IFilterConfig, value: any) => {
191
+ const handleFilterValues = async (
192
+ config: IFilterConfig,
193
+ value: any,
194
+ formRelationships?: IFormRelationshipConfig[],
195
+ ) => {
167
196
  const isNumber = typeof value === 'number'
168
197
  const isBoolean = typeof value === 'boolean'
169
198
 
@@ -171,6 +200,31 @@ const handleFilterValues = async (config: IFilterConfig, value: any) => {
171
200
 
172
201
  if (config.type === FilterConfigTypeEnum.NoFilter) return null
173
202
 
203
+ if (config.type === FilterConfigTypeEnum.RelatedChildForm) {
204
+ const childFormId = typeof value === 'number' ? value : parseInt(value, 10)
205
+ const relationship = formRelationships?.find(
206
+ (rel) => rel.type === FormRelationshipEnum.OneToMany && rel.formId === childFormId,
207
+ )
208
+
209
+ if (!childFormId || !relationship) return null
210
+
211
+ return {
212
+ match: JSON.stringify({
213
+ [`${childFormId}._id`]: { $ne: null },
214
+ [`${childFormId}.DeletedDate`]: null,
215
+ }),
216
+ joins: [
217
+ {
218
+ formId: childFormId,
219
+ localField: '_id',
220
+ foreignField: `Data.${relationship.foreignKey}`,
221
+ alias: `${childFormId}`,
222
+ },
223
+ ],
224
+ pipeline: JSON.stringify([{ $group: { _id: '$_id', doc: { $first: '$$ROOT' } } }]),
225
+ }
226
+ }
227
+
174
228
  if ((config as IFilterCustom).bsonDataType === BSON_DATA_IDENTIFIER_PREFIXES.Date) {
175
229
  const isRangeFilter =
176
230
  Array.isArray(value) && value.every((v) => [null, undefined].includes(v) || dayjs(v).isValid())
@@ -204,6 +258,10 @@ const handleFilterValues = async (config: IFilterConfig, value: any) => {
204
258
 
205
259
  if (value2) stringifiedFilter = stringifiedFilter.replaceAll(VALUE_REPLACEMENT_PLACEHOLDER2, value2)
206
260
 
207
- return stringifiedFilter
261
+ return {
262
+ match: stringifiedFilter,
263
+ pipeline: (config as IFilterCustom).pipeline,
264
+ joins: (config as IFilterCustom).joins,
265
+ }
208
266
  }
209
267
  }
@@ -201,9 +201,14 @@ export default function FormDataListTableComponent({
201
201
  <>
202
202
  <FormDataListHeaderComponent
203
203
  layoutConfig={layoutsConfigs?.dataListConfig.header}
204
- updateDynamicFilter={(match) => {
204
+ updateDynamicFilter={(data) => {
205
205
  setParentLoading(true)
206
- setFilterReqData((c) => ({ ...c, match }))
206
+ setFilterReqData((c) => ({
207
+ ...c,
208
+ match: data?.match,
209
+ pipeline: data?.pipeline,
210
+ joins: data?.joins,
211
+ }))
207
212
  }}
208
213
  dataCount={dataListOtherConfigs?.showCount ? (parentLoadings.data ? 'pending' : dataList.total) : undefined}
209
214
  headerLayoutContext={headerLayoutContext}
@@ -281,6 +286,8 @@ function normalizeFilter(filter?: IDataListReqData & { skip?: number }) {
281
286
  skip: typeof filter?.skip === 'number' ? filter.skip : undefined,
282
287
  sort: filter?.sort ?? undefined,
283
288
  match: filter?.match ?? undefined,
289
+ pipeline: filter?.pipeline ?? undefined,
290
+ joins: filter?.joins ? JSON.stringify(filter.joins) : undefined,
284
291
  }
285
292
  }
286
293
 
@@ -293,7 +300,9 @@ function areFiltersEqual(a?: IDataListReqData & { skip?: number }, b?: IDataList
293
300
  normalizedA.limit === normalizedB.limit &&
294
301
  normalizedA.skip === normalizedB.skip &&
295
302
  normalizedA.sort === normalizedB.sort &&
296
- normalizedA.match === normalizedB.match
303
+ normalizedA.match === normalizedB.match &&
304
+ normalizedA.pipeline === normalizedB.pipeline &&
305
+ normalizedA.joins === normalizedB.joins
297
306
  )
298
307
  }
299
308
 
@@ -4,7 +4,7 @@ import LayoutRendererCol from '../2-col'
4
4
  import { memo, ReactElement, useMemo } from 'react'
5
5
  import { LayoutRowConditionalHeaderRenderer } from './header-render'
6
6
  import { LayoutRowRepeatableRenderer } from './repeatable-render'
7
- import { IDndLayoutElement, IDndLayoutRow, IFormJoin } from '../../../../types'
7
+ import { IDndLayoutElement, IDndLayoutRow, IFormJoin, IFormRelationshipConfig } from '../../../../types'
8
8
  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'
@@ -108,5 +108,6 @@ export interface IFormContext {
108
108
  formId?: number
109
109
  detailPageFormId?: number
110
110
  parentFormJoins?: IFormJoin[]
111
+ formRelationships?: IFormRelationshipConfig[]
111
112
  manyToManyRelInfo?: { middleFormId: number; currentFormId: number; otherFormId: number } | null
112
113
  }
@@ -115,7 +115,7 @@ export default function GalleryDeleteModal({
115
115
  {discardText || 'Discard'}
116
116
  </Button_FillerPortal>
117
117
  <Button_FillerPortal primary loading={isDeleting} onClick={deletePictures}>
118
- {deleteText || 'Delete Selected'}
118
+ {deleteText || 'Delete selected'}
119
119
  </Button_FillerPortal>
120
120
  </div>
121
121
  }
@@ -3,7 +3,12 @@ import { memo, useMemo, useState } from 'react'
3
3
  import { FaPlus, FaTrashAlt } from 'react-icons/fa'
4
4
  import { FaUpload } from 'react-icons/fa6'
5
5
  import { isNewFormDataPage, mapToFormItemRules, resolveConditionalText } from '../../../../../functions'
6
- import { DeviceBreakpointEnum, FormElementConditionalKeyEnum, TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../../enums'
6
+ import {
7
+ DeviceBreakpointEnum,
8
+ FormElementConditionalKeyEnum,
9
+ TranslationTextSubTypeEnum,
10
+ TranslationTextTypeEnum,
11
+ } from '../../../../../enums'
7
12
  import { IGalleryElementProps, IValidationRule } from '../../../../../types'
8
13
  import { Button_FillerPortal } from '../../../../common/button'
9
14
  import { useTranslation } from '../../../../common/custom-hooks'
@@ -66,10 +71,11 @@ function LayoutRenderer_Gallery({
66
71
  }),
67
72
  [allValues, currentBreakpoint, elementKey, formDataId, t, textConditions],
68
73
  )
69
- const [emptyDescription = 'No pictures uploaded'] = useMemo(
74
+ const [emptyDescription, deleteButtonText] = useMemo(
70
75
  () =>
71
76
  t([
72
77
  { key: elementKey, type: TranslationTextTypeEnum.Description, subType: TranslationTextSubTypeEnum.EmptyState },
78
+ { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.DeleteButton },
73
79
  ]),
74
80
  [elementKey, t],
75
81
  )
@@ -156,7 +162,7 @@ function LayoutRenderer_Gallery({
156
162
  className="text-danger underline hover:text-danger"
157
163
  >
158
164
  <FaTrashAlt />
159
- Delete
165
+ {deleteButtonText || 'Delete selected'}
160
166
  </Button_FillerPortal>
161
167
  )}
162
168
  {!isUploadButtonHidden && (
@@ -215,7 +221,7 @@ function LayoutRenderer_Gallery({
215
221
  </div>
216
222
  </Image.PreviewGroup>
217
223
  ) : (
218
- <Empty description={emptyDescription || undefined} />
224
+ <Empty description={emptyDescription || 'No pictures uploaded'} />
219
225
  )}
220
226
  </Form.Item>
221
227
  <GalleryUploadModal
@@ -24,6 +24,7 @@ import {
24
24
  PageViewTypEnum,
25
25
  FieldElementOptionSourceEnum,
26
26
  FilterConfigTypeEnum,
27
+ FormRelationshipEnum,
27
28
  IdMatchFieldTypeEnum,
28
29
  TranslationTextTypeEnum,
29
30
  TranslationTextSubTypeEnum,
@@ -61,6 +62,8 @@ function LayoutRenderer_FieldsWithOptions({
61
62
  const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(
62
63
  props.optionSource.type === FieldElementOptionSourceEnum.ReadFromDetails
63
64
  ? props.optionSource?.baseFormId
65
+ : props.optionSource.type === FieldElementOptionSourceEnum.RelatedChildForm
66
+ ? formContext.formId
64
67
  : undefined,
65
68
  )
66
69
 
@@ -318,9 +321,22 @@ function LayoutRenderer_FieldsWithOptions({
318
321
  }),
319
322
  )
320
323
  setLoading(false)
324
+ } else if (props.optionSource.type === FieldElementOptionSourceEnum.RelatedChildForm) {
325
+ setRawOptions(
326
+ (cachedConfig?.relationships ?? [])
327
+ .filter((rel) => rel.type === FormRelationshipEnum.OneToMany)
328
+ .filter((rel, idx, arr) => arr.findIndex((eachRel) => eachRel.formId === rel.formId) === idx)
329
+ .map((rel) => {
330
+ const childFormId = rel.formId
331
+ const label = getFormById(childFormId)?.name || `Form #${childFormId}`
332
+
333
+ return { value: childFormId, label, fullLabel: label }
334
+ }),
335
+ )
336
+ setLoading(false)
321
337
  } else if (props.optionSource.type === FieldElementOptionSourceEnum.ReadFromDetails) fetchDetailsStaticOptions()
322
338
  else setLoading(false)
323
- }, [props.optionSource, fetchDetailsStaticOptions, t])
339
+ }, [props.optionSource, fetchDetailsStaticOptions, t, cachedConfig?.relationships, getFormById])
324
340
 
325
341
  useEffect(() => {
326
342
  return () => {
@@ -79,6 +79,7 @@ export enum FieldElementOptionSourceEnum {
79
79
  Any = 'Any',
80
80
  Static = 'Static',
81
81
  DynamicForm = 'DynamicForm',
82
+ RelatedChildForm = 'RelatedChildForm',
82
83
  ReadFromDetails = 'ReadFromDetails', // used in data list header
83
84
  Roles = 'Roles',
84
85
  }
@@ -124,6 +125,7 @@ export enum JustifyAndAlignContent {
124
125
  export enum FilterConfigTypeEnum {
125
126
  NoFilter = 'NoFilter',
126
127
  Custom = 'Custom',
128
+ RelatedChildForm = 'RelatedChildForm',
127
129
  }
128
130
  export enum DeviceBreakpointEnum {
129
131
  Default = 'default',
@@ -136,6 +136,7 @@ export enum TranslationTextSubTypeEnum {
136
136
  RepeatButtonRemove = 'RepeatRemove',
137
137
  Secondary = '2',
138
138
  EmptyState = 'EmptyState',
139
+ DeleteButton = 'DeleteButton',
139
140
  DeleteModalTitle = 'DeleteModalTitle',
140
141
  DeleteModalDiscard = 'DeleteModalDiscard',
141
142
  DeleteModalDelete = 'DeleteModalDelete',
@@ -1,5 +1,5 @@
1
1
  import { LOCAL_STORAGE_KEYS_ENUM } from '../../enums'
2
- import { IFormDataListData } from '../../types'
2
+ import { IFormDataListData, IFormJoin } from '../../types'
3
3
 
4
4
  type DataListSnapshot = { data: IFormDataListData[]; total: number }
5
5
 
@@ -91,6 +91,8 @@ export type IDataListFilterState = {
91
91
  limit?: number
92
92
  sort?: string
93
93
  match?: string
94
+ pipeline?: string
95
+ joins?: IFormJoin[]
94
96
  skip?: number
95
97
  }
96
98
 
@@ -23,6 +23,7 @@ import {
23
23
  DataRenderTypeEnum,
24
24
  ElementTypeEnum,
25
25
  FieldElementOptionSourceEnum,
26
+ FilterConfigTypeEnum,
26
27
  LOCAL_STORAGE_KEYS_ENUM,
27
28
  NotificationTypeEnum,
28
29
  TemplateDataReplacementFieldTypesEnum,
@@ -32,6 +33,7 @@ import {
32
33
  IEmail_Attachment,
33
34
  IFilterNested,
34
35
  IDndLayoutElement,
36
+ IFormLayoutFieldOption,
35
37
  IGridContainerConfig,
36
38
  IDynamicForm,
37
39
  IFormDataApiReqConfig,
@@ -45,13 +47,22 @@ export const extractFiltersFromLayout = (elements: { [key: string]: IDndLayoutEl
45
47
 
46
48
  Object.values(elements).forEach((el) => {
47
49
  if (
48
- el.elementType === ElementTypeEnum.Radio &&
50
+ [ElementTypeEnum.Radio, ElementTypeEnum.Select].includes(el.elementType) &&
51
+ el.props &&
52
+ 'optionSource' in el.props &&
49
53
  el.props.optionSource?.type === FieldElementOptionSourceEnum.Static
50
54
  ) {
51
- el.props.optionSource.options.forEach((op) => {
55
+ el.props.optionSource.options.forEach((op: IFormLayoutFieldOption) => {
52
56
  if (op.filter)
53
- filters[el.key] = filters[el.key] ? { ...filters[el.key], [op.value]: op.filter } : { [op.value]: op.filter }
57
+ filters[el.key] = filters[el.key] ? { ...filters[el.key], [op.id]: op.filter } : { [op.id]: op.filter }
54
58
  })
59
+ } else if (
60
+ el.elementType === ElementTypeEnum.Select &&
61
+ el.props &&
62
+ 'optionSource' in el.props &&
63
+ el.props.optionSource?.type === FieldElementOptionSourceEnum.RelatedChildForm
64
+ ) {
65
+ filters[el.key] = { type: FilterConfigTypeEnum.RelatedChildForm }
55
66
  } else if (el.props && 'filter' in el.props && el.props.filter) {
56
67
  filters[el.key] = {
57
68
  ...el.props.filter,
@@ -427,8 +438,8 @@ const emailFieldKeys: Array<'to' | 'from' | 'cc' | 'bcc'> = ['to', 'from', 'cc',
427
438
  const getValueFromPath = (path: string, data: Record<string, any>) => {
428
439
  if (!path) return undefined
429
440
  return path.split('.').reduce((acc: any, key: string) => {
430
- if (!acc) return undefined
431
- return acc[key] ?? acc
441
+ if (acc === undefined || acc === null) return undefined
442
+ return acc[key]
432
443
  }, data)
433
444
  }
434
445
 
@@ -1,13 +1,17 @@
1
1
  import { IFormJoin } from '..'
2
2
  import { FilterConfigTypeEnum } from '../../../enums'
3
3
 
4
- export type IFilterConfig = INoFilterConfig | IFilterCustom
4
+ export type IFilterConfig = INoFilterConfig | IFilterCustom | IFilterRelatedChildForm
5
5
  export interface INoFilterConfig {
6
6
  type: FilterConfigTypeEnum.NoFilter
7
7
  }
8
+ export interface IFilterRelatedChildForm {
9
+ type: FilterConfigTypeEnum.RelatedChildForm
10
+ }
8
11
  export interface IFilterCustom {
9
12
  type: FilterConfigTypeEnum.Custom
10
13
  config: { [key: string]: any }
14
+ pipeline?: string
11
15
  bsonDataType?: string
12
16
  joins?: IFormJoin[]
13
17
  conditionFormBranches?: string[]
@@ -3,6 +3,7 @@ import { FieldElementOptionSourceEnum } from '../../../enums'
3
3
 
4
4
  export type IFieldElementOptionSource =
5
5
  | IOptionSourceDynamicForm
6
+ | IOptionSourceRelatedChildForm
6
7
  | IOptionSourceConstant // constant options - used in details page
7
8
  | IOptionSourceReadFromDetails // reading constant options (above) in list header
8
9
  | IOptionSourceAny // no options configured, but users can enter any options for informational purpose only
@@ -27,6 +28,10 @@ export type IOptionSourceDynamicForm = {
27
28
  }
28
29
  } & IDataFetchConfig
29
30
 
31
+ export interface IOptionSourceRelatedChildForm {
32
+ type: FieldElementOptionSourceEnum.RelatedChildForm
33
+ }
34
+
30
35
  export interface IOptionSourceReadFromDetails {
31
36
  type: FieldElementOptionSourceEnum.ReadFromDetails
32
37
  formId: number