form-craft-package 1.0.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.
Files changed (40) hide show
  1. package/.prettierrc +9 -0
  2. package/AJV_JSON_Schema_Guide.md +409 -0
  3. package/README.md +108 -0
  4. package/index.ts +8 -0
  5. package/package.json +40 -0
  6. package/src/ajv/form/form.schema.json +10 -0
  7. package/src/ajv/form/layout.schema.json +97 -0
  8. package/src/ajv/form/migration-rules.schema.json +59 -0
  9. package/src/ajv/master-portal-only/render-conditions/conditions.schema.json +24 -0
  10. package/src/ajv/master-portal-only/render-conditions/validate.ts +15 -0
  11. package/src/components/common/button.tsx +72 -0
  12. package/src/components/common/custom-hooks/use-find-dynamic-form.ts +33 -0
  13. package/src/components/common/custom-hooks/use-lazy-modal-opener.hook.ts +20 -0
  14. package/src/components/common/custom-hooks/use-notification.hook.tsx +157 -0
  15. package/src/components/common/disabled-field-indicator.tsx +20 -0
  16. package/src/components/common/warning-icon.tsx +10 -0
  17. package/src/components/form/layout-renderer/1-row/index.tsx +27 -0
  18. package/src/components/form/layout-renderer/2-col/index.tsx +32 -0
  19. package/src/components/form/layout-renderer/3-element/1-dynamic-button.tsx +277 -0
  20. package/src/components/form/layout-renderer/3-element/2-field-element.tsx +220 -0
  21. package/src/components/form/layout-renderer/3-element/index.tsx +73 -0
  22. package/src/components/index.tsx +2 -0
  23. package/src/components/modals/form-data-loading.modal.tsx +48 -0
  24. package/src/constants.ts +15 -0
  25. package/src/enums.ts +177 -0
  26. package/src/functions/axios-handler.ts +158 -0
  27. package/src/functions/data-list-functions.tsx +41 -0
  28. package/src/functions/form-schema-validator.ts +50 -0
  29. package/src/functions/get-element-props.ts +20 -0
  30. package/src/functions/index.ts +56 -0
  31. package/src/functions/json-handlers.ts +19 -0
  32. package/src/functions/validations.ts +120 -0
  33. package/src/types/form-data-list/index.ts +54 -0
  34. package/src/types/index.ts +124 -0
  35. package/src/types/layout-elements/element-data-render-logic.ts +56 -0
  36. package/src/types/layout-elements/field-option-source.ts +14 -0
  37. package/src/types/layout-elements/index.ts +224 -0
  38. package/src/types/layout-elements/style.ts +35 -0
  39. package/src/types/layout-elements/validation.ts +18 -0
  40. package/tsconfig.json +111 -0
@@ -0,0 +1,59 @@
1
+ {
2
+ "$id": "migration-rules.schema",
3
+ "type": "object",
4
+ "patternProperties": {
5
+ ".*": {
6
+ "type": "array",
7
+ "items": { "$ref": "#/definitions/IMigrationRule" }
8
+ }
9
+ },
10
+ "definitions": {
11
+ "IMigrationRule": {
12
+ "type": "object",
13
+ "oneOf": [
14
+ {
15
+ "properties": {
16
+ "operation": { "type": "string", "enum": ["RenameField"] },
17
+ "from": { "type": "string" },
18
+ "to": { "type": "string" }
19
+ },
20
+ "required": ["operation", "from", "to"]
21
+ },
22
+ {
23
+ "properties": {
24
+ "operation": { "type": "string", "enum": ["RemoveField"] },
25
+ "field": { "type": "string" }
26
+ },
27
+ "required": ["operation", "field"]
28
+ },
29
+ {
30
+ "properties": {
31
+ "operation": { "type": "string", "enum": ["ConvertDataType"] },
32
+ "field": { "type": "string" },
33
+ "fromType": { "type": "string" },
34
+ "toType": { "type": "string" },
35
+ "defaultValue": {}
36
+ },
37
+ "required": ["operation", "field", "fromType", "toType"]
38
+ },
39
+ {
40
+ "properties": {
41
+ "operation": { "type": "string", "enum": ["MakeRequired"] },
42
+ "field": { "type": "string" },
43
+ "defaultValue": {}
44
+ },
45
+ "required": ["operation", "field"]
46
+ },
47
+ {
48
+ "properties": {
49
+ "operation": { "type": "string", "enum": ["ValidationRuleChange"] },
50
+ "field": { "type": "string" },
51
+ "minLength": { "type": "integer" },
52
+ "defaultValue": {}
53
+ },
54
+ "required": ["operation", "field"]
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "render-conditions.schema",
4
+ "type": "array",
5
+ "items": {
6
+ "type": "object",
7
+ "properties": {
8
+ "when": { "type": "string" },
9
+ "outcomes": {
10
+ "type": "array",
11
+ "items": {
12
+ "type": "object",
13
+ "properties": {
14
+ "value": { "type": ["string", "number", "boolean"] },
15
+ "className": { "type": "string" },
16
+ "content": { "type": "string" }
17
+ },
18
+ "required": ["value", "content"]
19
+ }
20
+ }
21
+ },
22
+ "required": ["when", "outcomes"]
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ import Ajv, { ErrorObject } from 'ajv'
2
+ import conditionsSchema from './conditions.schema.json'
3
+ import { IDataCondition_Render } from '../../../types'
4
+
5
+ export const validateRenderConditionsData = (data: IDataCondition_Render) => {
6
+ const ajv = new Ajv({ allowUnionTypes: true })
7
+ const validate = ajv.compile(conditionsSchema)
8
+
9
+ const isValid = validate(data)
10
+
11
+ let errors: ErrorObject[] = []
12
+ if (!isValid) errors = validate.errors || []
13
+
14
+ return { isValid, errors }
15
+ }
@@ -0,0 +1,72 @@
1
+ import { Button } from 'antd'
2
+
3
+ type ButtonProps = {
4
+ primary?: boolean
5
+ secondary?: boolean
6
+ danger?: boolean
7
+ link?: boolean
8
+ icon?: boolean
9
+ outline?: boolean
10
+ short?: boolean
11
+ disabled?: boolean
12
+ loading?: boolean
13
+ className?: string
14
+ title?: string
15
+ children: any
16
+ } & (
17
+ | { onClick: (e: React.MouseEvent) => void; isFormSubmit?: boolean; form?: string }
18
+ | { onClick?: (e: React.MouseEvent) => void; isFormSubmit: boolean; form: string }
19
+ )
20
+
21
+ // FP = Filler Portal
22
+ export const ButtonFP = ({
23
+ primary,
24
+ danger,
25
+ link,
26
+ icon,
27
+ outline = false,
28
+ disabled = false,
29
+ loading = false,
30
+ short = false,
31
+ className,
32
+ children,
33
+ title = '',
34
+ form,
35
+ isFormSubmit = false,
36
+ onClick,
37
+ }: ButtonProps): JSX.Element => {
38
+ const defaultClass = `${outline ? 'btn-outline' : ''} ${short ? 'btn-short text-12' : ''}`
39
+ if (!className) className = defaultClass
40
+ else className = `${className} ${defaultClass}`
41
+
42
+ const buttonProps = {
43
+ disabled,
44
+ loading,
45
+ title,
46
+ className,
47
+ form,
48
+ onClick: (e: React.MouseEvent) => onClick && onClick(e),
49
+ }
50
+ const htmlTypeProp = isFormSubmit ? 'submit' : 'button'
51
+
52
+ if (primary || link)
53
+ return (
54
+ <Button type={primary ? 'primary' : 'link'} {...buttonProps} htmlType={htmlTypeProp}>
55
+ <div className="flex items-center gap-2">{children}</div>
56
+ </Button>
57
+ )
58
+
59
+ if (icon)
60
+ return (
61
+ <Button {...buttonProps} className="btn-icon" htmlType={htmlTypeProp}>
62
+ <div className="p-1 rounded-full">{children}</div>
63
+ </Button>
64
+ )
65
+
66
+ // secondary || danger || default
67
+ return (
68
+ <Button danger={danger ?? false} {...buttonProps} htmlType={htmlTypeProp}>
69
+ <div className="flex items-center gap-2">{children}</div>
70
+ </Button>
71
+ )
72
+ }
@@ -0,0 +1,33 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useParams } from 'react-router-dom'
3
+ import { LOCAL_STORAGE_KEYS_ENUM } from '../../../constants'
4
+
5
+ export const useFindDynamiForm = () => {
6
+ const { formName } = useParams<{ formName: string }>()
7
+ const [foundItem, setFoundItem] = useState<IDynamicForm | undefined | null>(undefined)
8
+
9
+ useEffect(() => {
10
+ if (!formName) return
11
+
12
+ try {
13
+ const storedData = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.DynamicForms)
14
+ if (storedData) {
15
+ const parsedData: IDynamicForm[] = JSON.parse(storedData)
16
+ const item = parsedData.find((entry) => entry.name.split(' ').join('-').toLocaleLowerCase() === formName)
17
+ setFoundItem(item ?? null)
18
+ }
19
+ } catch (error) {
20
+ console.error('Error reading or parsing localStorage data', error)
21
+ }
22
+ }, [formName])
23
+
24
+ return foundItem
25
+ }
26
+
27
+ interface IDynamicForm {
28
+ id: number
29
+ keepVersionHistory: boolean
30
+ logo: string | null
31
+ name: string
32
+ version: number
33
+ }
@@ -0,0 +1,20 @@
1
+ import { useState, useTransition } from 'react'
2
+
3
+ export const useLazyModalOpener = () => {
4
+ const [isModalOpen, setIsModalOpen] = useState(false)
5
+ const [isPendingTransition, startTransition] = useTransition()
6
+
7
+ const openModal = () => {
8
+ startTransition(() => {
9
+ setIsModalOpen(true)
10
+ })
11
+ }
12
+
13
+ const closeModal = () => {
14
+ startTransition(() => {
15
+ setIsModalOpen(false)
16
+ })
17
+ }
18
+
19
+ return { isModalOpen, isPendingTransition, openModal, closeModal }
20
+ }
@@ -0,0 +1,157 @@
1
+ import React, { createContext, ReactNode, useContext } from 'react'
2
+ import { Modal, notification, NotificationArgsProps } from 'antd'
3
+
4
+ type ModalOptions = {
5
+ title?: string
6
+ content?: React.ReactNode
7
+ okText?: string
8
+ cancelText?: string
9
+ centered?: boolean
10
+ width?: number | string
11
+ onOk?: () => Promise<void> | void
12
+ onCancel?: () => void
13
+ }
14
+
15
+ const defaultOptions: Partial<ModalOptions> = {
16
+ title: 'Confirmation',
17
+ content: 'Are you sure to proceed?',
18
+ okText: 'Continue',
19
+ cancelText: 'Cancel',
20
+ width: 520,
21
+ }
22
+
23
+ interface NotificationContextType {
24
+ openNotification: (options: NotificationArgsProps) => void
25
+ success: (options: NotificationArgsProps) => void
26
+ error: (options: NotificationArgsProps) => void
27
+ warning: (options: NotificationArgsProps) => void
28
+ destroy: () => void
29
+ confirmModal: (options: ModalOptions) => void
30
+ warningModal: (options: ModalOptions) => void
31
+ infoModal: (options: ModalOptions) => void
32
+ errorModal: (options: ModalOptions) => void
33
+ }
34
+
35
+ const NotificationContext = createContext<NotificationContextType | undefined>(undefined)
36
+
37
+ let notificationInstance: NotificationContextType | null = null
38
+
39
+ export const AntdNotificationProvider = ({ children }: { children: ReactNode }) => {
40
+ const [notificationApi, notificationContextHolder] = notification.useNotification()
41
+ const [modalApi, modalContextHolder] = Modal.useModal()
42
+
43
+ const openNotification = (options: NotificationArgsProps) => {
44
+ notificationApi.open(options)
45
+ }
46
+
47
+ const success = (options: NotificationArgsProps) => {
48
+ notificationApi.success({ ...options, onClick: () => notificationApi.destroy() })
49
+ }
50
+
51
+ const error = (options: NotificationArgsProps) => {
52
+ notificationApi.error({ ...options, onClick: () => notificationApi.destroy() })
53
+ }
54
+
55
+ const warning = (options: NotificationArgsProps) => {
56
+ notificationApi.warning({ ...options, onClick: () => notificationApi.destroy() })
57
+ }
58
+
59
+ const destroy = () => notificationApi.destroy()
60
+
61
+ const confirmModal = (options: ModalOptions) => {
62
+ modalApi.confirm({
63
+ ...defaultOptions,
64
+ ...options,
65
+ onOk: async () => {
66
+ try {
67
+ if (options.onOk) await options.onOk()
68
+ } catch (error) {
69
+ console.error('Error in onOk:', error)
70
+ }
71
+ },
72
+ onCancel: options.onCancel || (() => {}),
73
+ })
74
+ }
75
+
76
+ const warningModal = (options: ModalOptions) => {
77
+ modalApi.warning({
78
+ ...defaultOptions,
79
+ ...options,
80
+ onOk: async () => {
81
+ try {
82
+ if (options.onOk) await options.onOk()
83
+ } catch (error) {
84
+ console.error('Error in onOk:', error)
85
+ }
86
+ },
87
+ onCancel: options.onCancel || (() => {}),
88
+ })
89
+ }
90
+
91
+ const infoModal = (options: ModalOptions) => {
92
+ modalApi.info({
93
+ ...defaultOptions,
94
+ ...options,
95
+ onOk: async () => {
96
+ try {
97
+ if (options.onOk) await options.onOk()
98
+ } catch (error) {
99
+ console.error('Error in onOk:', error)
100
+ }
101
+ },
102
+ onCancel: options.onCancel || (() => {}),
103
+ })
104
+ }
105
+
106
+ const errorModal = (options: ModalOptions) => {
107
+ modalApi.error({
108
+ ...defaultOptions,
109
+ ...options,
110
+ onOk: async () => {
111
+ try {
112
+ if (options.onOk) await options.onOk()
113
+ } catch (error) {
114
+ console.error('Error in onOk:', error)
115
+ }
116
+ },
117
+ onCancel: options.onCancel || (() => {}),
118
+ })
119
+ }
120
+
121
+ const instance = {
122
+ openNotification,
123
+ success,
124
+ error,
125
+ warning,
126
+ destroy,
127
+ confirmModal,
128
+ warningModal,
129
+ infoModal,
130
+ errorModal,
131
+ }
132
+
133
+ notificationInstance = instance
134
+
135
+ return (
136
+ <NotificationContext.Provider value={instance}>
137
+ {notificationContextHolder}
138
+ {modalContextHolder}
139
+ {children}
140
+ </NotificationContext.Provider>
141
+ )
142
+ }
143
+
144
+ export const useNotification = () => {
145
+ const context = useContext(NotificationContext)
146
+ if (!context) {
147
+ throw new Error('useGlobalNotification must be used within a NotificationProvider')
148
+ }
149
+ return context
150
+ }
151
+
152
+ export const getNotificationInstance = (): NotificationContextType => {
153
+ if (!notificationInstance) {
154
+ throw new Error('AntdNotificationProvider is not initialized.')
155
+ }
156
+ return notificationInstance
157
+ }
@@ -0,0 +1,20 @@
1
+ import { FaPencilAlt, FaSlash } from 'react-icons/fa'
2
+
3
+ export const DisabledFieldIndicator = ({
4
+ top = 30,
5
+ right = 15,
6
+ left,
7
+ }: {
8
+ top?: number
9
+ right?: number
10
+ left?: number
11
+ }) => (
12
+ <div style={{ position: 'absolute', top, right, left, cursor: 'no-drop', color: '#a1a1a1' }}>
13
+ <div className="absolute">
14
+ <FaPencilAlt />
15
+ </div>
16
+ <div>
17
+ <FaSlash />
18
+ </div>
19
+ </div>
20
+ )
@@ -0,0 +1,10 @@
1
+ import { Tooltip } from 'antd'
2
+ import { FaExclamationTriangle } from 'react-icons/fa'
3
+
4
+ export default function WarningIcon({ tooltip = '', size = 16 }: { tooltip: any; size?: number }) {
5
+ return (
6
+ <Tooltip title={tooltip}>
7
+ <FaExclamationTriangle className="text-warning" size={size} />
8
+ </Tooltip>
9
+ )
10
+ }
@@ -0,0 +1,27 @@
1
+ import { ResponsivenessDeviceEnum } from '../../../../enums'
2
+ import { getFlexContainerStyle, getFlexItemStyle } from '../../../../functions'
3
+ import { IDataRender_ButtonProps, IFormLayoutRow } from '../../../../types'
4
+ import LayoutRendererCol from '../2-col'
5
+ import { ReactElement, ReactNode } from 'react'
6
+
7
+ export const LayoutRendererRow = ({
8
+ rowData,
9
+ titleComponent,
10
+ breakpointDevice = ResponsivenessDeviceEnum.Default,
11
+ renderButton,
12
+ }: {
13
+ rowData: IFormLayoutRow
14
+ titleComponent?: ReactNode
15
+ breakpointDevice?: ResponsivenessDeviceEnum
16
+ renderButton?: (props: IDataRender_ButtonProps) => ReactElement
17
+ }) => {
18
+ return (
19
+ <div style={{ ...(rowData.style ?? {}), ...getFlexContainerStyle(breakpointDevice, rowData.responsiveness) }}>
20
+ {rowData.children.map((col, colIdx) => (
21
+ <div key={colIdx} style={getFlexItemStyle(breakpointDevice, colIdx, rowData.responsiveness)}>
22
+ <LayoutRendererCol key={colIdx} colData={col} titleComponent={titleComponent} renderButton={renderButton} />
23
+ </div>
24
+ ))}
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,32 @@
1
+ import { ReactElement, ReactNode } from 'react'
2
+ import { LayoutRendererRow } from '../1-row'
3
+ import LayoutRendererElement from '../3-element'
4
+ import { IDataRender_ButtonProps, IFormLayoutCol, IFormLayoutElement, IFormLayoutRow } from '../../../../types'
5
+ import { FormLayoutNodeEnum } from '../../../../enums'
6
+
7
+ export default function LayoutRendererCol({
8
+ colData,
9
+ titleComponent,
10
+ renderButton,
11
+ }: {
12
+ colData: IFormLayoutCol
13
+ titleComponent?: ReactNode
14
+ renderButton?: (props: IDataRender_ButtonProps) => ReactElement
15
+ }) {
16
+ return (
17
+ <div className="space-y-2">
18
+ {colData.children.map((item: IFormLayoutRow | IFormLayoutElement) =>
19
+ item.nodeType === FormLayoutNodeEnum.Row ? (
20
+ <LayoutRendererRow key={item.id} rowData={item} titleComponent={titleComponent} renderButton={renderButton} />
21
+ ) : (
22
+ <div className="flex flex-col" key={item.id}>
23
+ <LayoutRendererElement elementData={item} titleComponent={titleComponent} renderButton={renderButton} />
24
+ {item.props && 'description' in item.props && (
25
+ <span className="text-neutral text-12 italic">{item.props.description}</span>
26
+ )}
27
+ </div>
28
+ ),
29
+ )}
30
+ </div>
31
+ )
32
+ }