form-craft-package 1.9.2 → 1.9.3-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.9.2",
3
+ "version": "1.9.3-dev.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -1,6 +1,6 @@
1
1
  import { DeviceBreakpointEnum, FormElementConditionalKeyEnum, FormPreservedItemKeys } from '../../../../enums'
2
2
  import { isValidationsMet } from './visibility-utils'
3
- import { useEffect } from 'react'
3
+ import { useLayoutEffect } from 'react'
4
4
  import useGetCurrentBreakpoint from '../use-window-width.hook'
5
5
  import { Form, FormInstance } from 'antd'
6
6
  import { hiddenStore } from './visibility-store'
@@ -19,7 +19,7 @@ export function useSetHiddenNodes(
19
19
  const currentBreakpoint = useGetCurrentBreakpoint()
20
20
  const isFormValuesSet = Form.useWatch(FormPreservedItemKeys.IsDetailsDataSet, { form: formDataRef, preserve: true })
21
21
 
22
- useEffect(() => {
22
+ useLayoutEffect(() => {
23
23
  // during the initial renders, handle the initially hidden nodes
24
24
  if (!formDataRef) return
25
25
 
@@ -1,4 +1,4 @@
1
- import { useSyncExternalStore } from 'react'
1
+ import { useLayoutEffect, useState, useSyncExternalStore } from 'react'
2
2
  import { disabledStore, hiddenStore } from './visibility-store'
3
3
 
4
4
  export const useIsNodeHidden = (id: string): boolean =>
@@ -8,7 +8,24 @@ export const useIsNodeHidden = (id: string): boolean =>
8
8
  () => hiddenStore.getSnapshot().includes(id),
9
9
  )
10
10
 
11
- export const checkIsNodeHidden = (id: string): boolean => hiddenStore.getSnapshot().includes(id)
11
+ // export const checkIsNodeHidden = (id: string): boolean => hiddenStore.getSnapshot().includes(id)
12
+ const arraysEqual = (a: string[], b: string[]) => {
13
+ if (a.length !== b.length) return false
14
+ return a.every((x, i) => x === b[i])
15
+ }
16
+ export function useHiddenIds(): string[] {
17
+ const [ids, setIds] = useState<string[]>(() => hiddenStore.getSnapshot())
18
+
19
+ useLayoutEffect(() => {
20
+ const unsubscribe = hiddenStore.subscribe(() => {
21
+ const next = hiddenStore.getSnapshot()
22
+ setIds((prev) => (arraysEqual(prev, next) ? prev : next))
23
+ })
24
+ return unsubscribe
25
+ }, [])
26
+
27
+ return ids
28
+ }
12
29
 
13
30
  export const useIsNodeDisabled = (id: string): boolean =>
14
31
  useSyncExternalStore(
@@ -20,7 +20,23 @@ export function ConfigProviderLayout({ children }: { children: ReactNode }): JSX
20
20
 
21
21
  return (
22
22
  <ConfigContext.Provider value={{ config }}>
23
- <ConfigProvider theme={theme}>
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
+ >
24
40
  {showSwitchCompany && (
25
41
  <div className="absolute top-2 right-2 z-20">
26
42
  <Button_FillerPortal
@@ -7,19 +7,15 @@ import { LayoutRowRepeatableRenderer } from './repeatable-render'
7
7
  import { IDndLayoutElement, IDndLayoutRow, IFormJoin } from '../../../../types'
8
8
  import { IDynamicButton_DisplayStateProps } from '../3-element/1-dynamic-button'
9
9
  import { ELEMENTS_DEFAULT_CLASS } from '../../../../constants'
10
- import {
11
- checkIsNodeHidden,
12
- useIsNodeHidden,
13
- } from '../../../common/custom-hooks/use-node-condition.hook/use-node-condition.hook'
10
+ import { useHiddenIds } from '../../../common/custom-hooks/use-node-condition.hook/use-node-condition.hook'
14
11
 
15
12
  export const LayoutRendererRow = memo(
16
13
  ({ basePath = [], rowData, dataCount, formContext, elements, renderButton }: ILayoutRendererRow) => {
17
- const isHidden = useIsNodeHidden(rowData.id)
18
-
14
+ const hiddenIds = useHiddenIds()
19
15
 
20
16
  return (
21
17
  <div
22
- style={{ display: isHidden ? 'none' : undefined }}
18
+ style={{ display: hiddenIds.includes(rowData.id) ? 'none' : undefined }}
23
19
  id={rowData.id}
24
20
  className={ELEMENTS_DEFAULT_CLASS.LayoutRowContainer}
25
21
  >
@@ -41,7 +37,7 @@ export const LayoutRendererRow = memo(
41
37
  ...getGridContainerStyle(rowData.display),
42
38
  }
43
39
  const hiddenColsIndices = rowData.children.reduce(
44
- (curr: number[], col, colIdx) => (checkIsNodeHidden(col.id) ? [...curr, colIdx] : curr),
40
+ (curr: number[], col, colIdx) => (hiddenIds.includes(col.id) ? [...curr, colIdx] : curr),
45
41
  [],
46
42
  )
47
43
 
@@ -55,7 +51,7 @@ export const LayoutRendererRow = memo(
55
51
 
56
52
  return (
57
53
  <div
58
- className={`${ELEMENTS_DEFAULT_CLASS.LayoutRow} ${isHidden ? 'hidden' : ''}`}
54
+ className={`${ELEMENTS_DEFAULT_CLASS.LayoutRow} ${hiddenIds.includes(rowData.id) ? 'hidden' : ''}`}
59
55
  style={{ ...style, gridTemplateColumns, maxWidth: '100vw', overflowX: 'auto' }}
60
56
  >
61
57
  {rowData.children.map((col, colIdx) => (
@@ -266,9 +266,9 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
266
266
  >
267
267
  {isPublic &&
268
268
  isNewFormDataPage(formDataId) &&
269
- btnProps.category !== ButtonActionCategoryEnum.SaveDataChanges && (
270
- <WarningIcon tooltip={BUTTON_CUSTOM_ERROR_MESSAGES.UnavailableForPublic} />
271
- )}
269
+ ![ButtonActionCategoryEnum.SaveDataChanges, ButtonActionCategoryEnum.Navigate].includes(
270
+ btnProps.category,
271
+ ) && <WarningIcon tooltip={BUTTON_CUSTOM_ERROR_MESSAGES.UnavailableForPublic} />}
272
272
  {t({ key: btnKey, type: TranslationTextTypeEnum.Label })}
273
273
  </Button_FillerPortal>
274
274
  {!!dataLoadingType && (
@@ -0,0 +1,10 @@
1
+ .ant-select.ant-select-outlined.fc-breadcrumb.responsive {
2
+ .ant-select-selector {
3
+ @apply bg-transparent border-none;
4
+ }
5
+ &.ant-select-focused {
6
+ .ant-select-selector {
7
+ @apply shadow-none;
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,100 @@
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 } from 'react'
6
+ import { ELEMENTS_DEFAULT_CLASS } from '../../../../../constants'
7
+ import { IElementBaseProps } from '../'
8
+ import { useTranslation } from '../../../../common/custom-hooks/use-translation.hook/hook'
9
+ import useGetCurrentBreakpoint from '../../../../common/custom-hooks/use-window-width.hook'
10
+ import { Select } from 'antd'
11
+ import {
12
+ DeviceBreakpointEnum,
13
+ PageViewTypEnum,
14
+ TranslationTextSubTypeEnum,
15
+ TranslationTextTypeEnum,
16
+ } from '../../../../../enums'
17
+
18
+ import './index.scss'
19
+
20
+ function LayoutRenderer_Breadcrumb({ formId, elementKey }: { formId?: number } & IElementBaseProps) {
21
+ const currentBreakpoint = useGetCurrentBreakpoint()
22
+ const { t } = useTranslation(formId)
23
+ const navigate = useNavigate()
24
+ const { breadcrumbs, sliceAt } = useBreadcrumb()
25
+
26
+ const [detailText, newText, listText] = t([
27
+ { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.BreadcrumbDetail },
28
+ { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.BreadcrumbNew },
29
+ { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.BreadcrumbList },
30
+ ])
31
+
32
+ const showAsSelect = useMemo(
33
+ () =>
34
+ [DeviceBreakpointEnum.Mobile, DeviceBreakpointEnum.TabletPortrait].includes(currentBreakpoint) &&
35
+ breadcrumbs.length > 1,
36
+ [currentBreakpoint, breadcrumbs],
37
+ )
38
+
39
+ if (showAsSelect)
40
+ return (
41
+ <Select
42
+ suffixIcon={null}
43
+ value={breadcrumbs[breadcrumbs.length - 1].href}
44
+ showSearch
45
+ optionFilterProp="label"
46
+ allowClear={false}
47
+ className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} responsive`}
48
+ onChange={(bc) => {
49
+ const decodedUri = decodeURIComponent(bc)
50
+ const bcIdx = breadcrumbs.findIndex((bc) => bc.href === decodedUri)
51
+
52
+ sliceAt(bcIdx)
53
+ navigate(decodedUri)
54
+ }}
55
+ options={breadcrumbs.map((bc) => ({
56
+ value: bc.href,
57
+ label: (
58
+ <div className="flex gap-2 items-center">
59
+ <FaChevronLeft className="text-primary" size={10} />
60
+ {getText(bc, { newText, detailText, listText })}
61
+ </div>
62
+ ),
63
+ }))}
64
+ />
65
+ )
66
+
67
+ return (
68
+ <div className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} flex items-center text-12`}>
69
+ {breadcrumbs.map((bc, bcIdx) => (
70
+ <React.Fragment key={bcIdx}>
71
+ <Button_FillerPortal
72
+ link
73
+ disabled={breadcrumbs.length - 1 === bcIdx}
74
+ onClick={() => {
75
+ sliceAt(bcIdx)
76
+ navigate(bc.href)
77
+ }}
78
+ >
79
+ {getText(bc, { newText, detailText, listText })}
80
+ </Button_FillerPortal>
81
+ {breadcrumbs.length - 1 !== bcIdx && <FaChevronRight className="text-primary" />}
82
+ </React.Fragment>
83
+ ))}
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export default memo(LayoutRenderer_Breadcrumb)
89
+
90
+ const getText = (bc: IBreadcrumb, texts: { newText: string; detailText: string; listText: string }) => (
91
+ <span className="font-normal italic">
92
+ {bc.type === PageViewTypEnum.New ? `${texts.newText} ` : ''}
93
+ {bc.label}
94
+ {bc.type === PageViewTypEnum.Details
95
+ ? ` ${texts.detailText}`
96
+ : bc.type === PageViewTypEnum.List
97
+ ? ` ${texts.listText}`
98
+ : ''}
99
+ </span>
100
+ )
@@ -264,7 +264,7 @@ interface IMapperFieldObj {
264
264
  nameFullPath?: string | (string | number)[]
265
265
  label?: string | ReactNode
266
266
  description?: string
267
- placeholder?: string | [string, string]
267
+ placeholder?: string | [string, string] | ReactNode
268
268
  disabled?: boolean
269
269
  options?: ISelectOption[]
270
270
  isMultiValue?: boolean
@@ -1,10 +1,4 @@
1
1
  import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
- import {
3
- PageViewTypEnum,
4
- FieldElementOptionSourceEnum,
5
- FilterConfigTypeEnum,
6
- TranslationTextTypeEnum,
7
- } from '../../../../enums'
8
2
  import { IElementBaseProps } from '.'
9
3
  import { LayoutRenderer_FieldElement } from './2-field-element'
10
4
  import { getElementGeneralizedProps } from '../../../../functions/forms/get-element-props'
@@ -16,6 +10,17 @@ import { useFindDynamiForm } from '../../../common/custom-hooks'
16
10
  import { useNavigate } from 'react-router-dom'
17
11
  import { constructDynamicFormHref, getIdEqualsQuery, isNewFormDataPage } from '../../../../functions'
18
12
  import { useCacheFormLayoutConfig } from '../../../common/custom-hooks/use-cache-form-layout-config.hook'
13
+ import { useTranslation } from '../../../common/custom-hooks/use-translation.hook/hook'
14
+ import useGetCurrentBreakpoint from '../../../common/custom-hooks/use-window-width.hook'
15
+ import {
16
+ PageViewTypEnum,
17
+ FieldElementOptionSourceEnum,
18
+ FilterConfigTypeEnum,
19
+ TranslationTextTypeEnum,
20
+ ElementTypeEnum,
21
+ DeviceBreakpointEnum,
22
+ TranslationTextSubTypeEnum,
23
+ } from '../../../../enums'
19
24
  import {
20
25
  IFilterConfig,
21
26
  IFormJoin,
@@ -29,7 +34,6 @@ import {
29
34
  ISelectElement,
30
35
  ISelectElementProps,
31
36
  } from '../../../../types'
32
- import { useTranslation } from '../../../common/custom-hooks/use-translation.hook/hook'
33
37
 
34
38
  function LayoutRenderer_FieldsWithOptions({
35
39
  formContext,
@@ -38,6 +42,7 @@ function LayoutRenderer_FieldsWithOptions({
38
42
  isDisabled,
39
43
  elementKey,
40
44
  }: ILayoutRenderer_FieldsWithOptions) {
45
+ const currentBreakpoint = useGetCurrentBreakpoint()
41
46
  const { t } = useTranslation(formContext.formId)
42
47
  const navigate = useNavigate()
43
48
  const { getFormById } = useFindDynamiForm()
@@ -178,7 +183,6 @@ function LayoutRenderer_FieldsWithOptions({
178
183
  const fetchDetailsStaticOptions = useCallback(
179
184
  (optionSource: IOptionSourceReadFromDetails) => {
180
185
  const { formId, optionFieldPath } = optionSource
181
- setLoading(false)
182
186
  if (!formId || !cachedConfig) {
183
187
  setLoading(false)
184
188
  return
@@ -191,11 +195,16 @@ function LayoutRenderer_FieldsWithOptions({
191
195
  const element = elements[field]
192
196
  const fieldOptionSource = (element as ISelectElement | IRadioElement).props?.optionSource as IOptionSourceConstant
193
197
  if (fieldOptionSource?.type === FieldElementOptionSourceEnum.Static && Array.isArray(fieldOptionSource.options))
194
- setOptions(fieldOptionSource.options.map((op) => ({ value: op.id, label: op.value })))
198
+ setOptions(
199
+ fieldOptionSource.options.map((op) => ({
200
+ value: op.id,
201
+ label: t({ key: element.key, type: TranslationTextTypeEnum.OptionValue, subType: op.id }),
202
+ })),
203
+ )
195
204
 
196
205
  setLoading(false)
197
206
  },
198
- [cachedConfig],
207
+ [cachedConfig, elementKey],
199
208
  )
200
209
 
201
210
  useEffect(() => {
@@ -236,11 +245,24 @@ function LayoutRenderer_FieldsWithOptions({
236
245
  }
237
246
  }, [])
238
247
 
239
- const [label, placeholder] = t([
248
+ const [label, placeholder, goToDetailsText] = t([
240
249
  { key: elementData.key, type: TranslationTextTypeEnum.Label },
241
250
  { key: elementData.key, type: TranslationTextTypeEnum.Placeholder },
251
+ { key: elementData.key, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.Secondary },
242
252
  ])
243
253
 
254
+ const type = useMemo(() => {
255
+ // converting Radio into Select on XS, and SM screens if the options are more than 2
256
+ if (
257
+ elementData.elementType === ElementTypeEnum.Radio &&
258
+ [DeviceBreakpointEnum.Mobile, DeviceBreakpointEnum.TabletPortrait].includes(currentBreakpoint) &&
259
+ options.length > 2
260
+ )
261
+ return ElementTypeEnum.Select
262
+
263
+ return elementData.elementType
264
+ }, [currentBreakpoint, elementData.elementType, options.length])
265
+
244
266
  return (
245
267
  <div className="relative">
246
268
  <LayoutRenderer_FieldElement formRef={formRef} elementKey={elementData.key} formId={formContext.formId}>
@@ -250,25 +272,23 @@ function LayoutRenderer_FieldsWithOptions({
250
272
  label: label && (
251
273
  <div className="flex items-center gap-2 w-full">
252
274
  <span>{label}</span>
253
- {(props as ISelectElementProps).goToDetails?.enabled &&
254
- (props as ISelectElementProps).goToDetails?.text &&
255
- selectedValue && (
256
- <div
257
- className="px-2 text-primary font-bold text-12 hover:underline cursor-pointer"
258
- onClick={() => {
259
- const formInfo = getFormById(props.optionSource.formId)
260
- if (!formInfo) return
261
-
262
- push({ label: formInfo.name, href: location.pathname, type: PageViewTypEnum.Details })
263
- navigate(`${constructDynamicFormHref(formInfo.name)}/${selectedValue}`)
264
- }}
265
- >
266
- {(props as ISelectElementProps).goToDetails!.text}
267
- </div>
268
- )}
275
+ {(props as ISelectElementProps).goToDetails?.enabled && goToDetailsText && selectedValue && (
276
+ <div
277
+ className="px-2 text-primary font-bold text-12 hover:underline cursor-pointer"
278
+ onClick={() => {
279
+ const formInfo = getFormById(props.optionSource.formId)
280
+ if (!formInfo) return
281
+
282
+ push({ label: formInfo.name, href: location.pathname, type: PageViewTypEnum.Details })
283
+ navigate(`${constructDynamicFormHref(formInfo.name)}/${selectedValue}`)
284
+ }}
285
+ >
286
+ {goToDetailsText}
287
+ </div>
288
+ )}
269
289
  </div>
270
290
  ),
271
- type: elementData.elementType,
291
+ type,
272
292
  name: formItem.name,
273
293
  validations: elementData.validations,
274
294
  options,
package/src/constants.ts CHANGED
@@ -26,7 +26,7 @@ export const LANGUAGE_LABELS = {
26
26
  [CountryEnum.CN]: 'Chinese',
27
27
  }
28
28
  export const DEVICE_BREAKPOINTS = {
29
- [DeviceBreakpointEnum.Mobile]: '576px',
29
+ [DeviceBreakpointEnum.Mobile]: '425px',
30
30
  [DeviceBreakpointEnum.TabletPortrait]: '768px',
31
31
  [DeviceBreakpointEnum.TabletLandscape]: '992px',
32
32
  [DeviceBreakpointEnum.Laptop]: '1200px',
@@ -1,51 +0,0 @@
1
- import { useBreadcrumb } from '../../../common/custom-hooks/use-breadcrumb.hook'
2
- import { Button_FillerPortal } from '../../../common/button'
3
- import { useNavigate } from 'react-router-dom'
4
- import { FaChevronRight } from 'react-icons/fa6'
5
- import React, { memo } from 'react'
6
- import { PageViewTypEnum, TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../enums'
7
- import { ELEMENTS_DEFAULT_CLASS } from '../../../../constants'
8
- import { IElementBaseProps } from '.'
9
- import { useTranslation } from '../../../common/custom-hooks/use-translation.hook/hook'
10
-
11
- function LayoutRenderer_Breadcrumb({ formId, elementKey }: { formId?: number } & IElementBaseProps) {
12
- const { t } = useTranslation(formId)
13
- const navigate = useNavigate()
14
- const { breadcrumbs, sliceAt } = useBreadcrumb()
15
-
16
- const [detailText, newText, listText] = t([
17
- { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.BreadcrumbDetail },
18
- { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.BreadcrumbNew },
19
- { key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.BreadcrumbList },
20
- ])
21
-
22
- return (
23
- <div className={`${ELEMENTS_DEFAULT_CLASS.Breadcrumb} flex items-center text-12`}>
24
- {breadcrumbs.map((bc, bcIdx) => (
25
- <React.Fragment key={bcIdx}>
26
- <Button_FillerPortal
27
- link
28
- disabled={breadcrumbs.length - 1 === bcIdx}
29
- onClick={() => {
30
- sliceAt(bcIdx)
31
- navigate(bc.href)
32
- }}
33
- >
34
- <span className="font-normal italic">
35
- {bc.type === PageViewTypEnum.New ? `${newText} ` : ''}
36
- {bc.label}
37
- {bc.type === PageViewTypEnum.Details
38
- ? ` ${detailText}`
39
- : bc.type === PageViewTypEnum.List
40
- ? ` ${listText}`
41
- : ''}
42
- </span>
43
- </Button_FillerPortal>
44
- {breadcrumbs.length - 1 !== bcIdx && <FaChevronRight className="text-primary" />}
45
- </React.Fragment>
46
- ))}
47
- </div>
48
- )
49
- }
50
-
51
- export default memo(LayoutRenderer_Breadcrumb)