dynamic-modal 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 (42) hide show
  1. package/README-ES.md +75 -0
  2. package/README.md +75 -0
  3. package/eslint.config.mjs +14 -0
  4. package/examples/enable-if.ts +0 -0
  5. package/examples/live-data.ts +63 -0
  6. package/examples/render-if.ts +0 -0
  7. package/examples/simple.ts +76 -0
  8. package/index.ts +3 -0
  9. package/package.json +47 -0
  10. package/src/components/input-upload/input-upload.tsx +71 -0
  11. package/src/components/make-autocomplete/make-autocomplete.tsx +51 -0
  12. package/src/components/make-button/make-button.tsx +17 -0
  13. package/src/components/make-input/make-input.tsx +44 -0
  14. package/src/components/make-multi-select/make-multi-select.tsx +53 -0
  15. package/src/components/make-select/make-select.tsx +51 -0
  16. package/src/components/make-text/make-text.tsx +16 -0
  17. package/src/components/make-textarea/make-textarea.tsx +45 -0
  18. package/src/components/make-title/make-title.tsx +12 -0
  19. package/src/components/make-toggle/make-toggle.tsx +44 -0
  20. package/src/components/make-upload/make-upload.tsx +40 -0
  21. package/src/components/portal/portal.tsx +36 -0
  22. package/src/hooks/field-render.ts +104 -0
  23. package/src/hooks/modal-handler.ts +37 -0
  24. package/src/interfaces/field.ts +30 -0
  25. package/src/interfaces/input-upload.ts +36 -0
  26. package/src/interfaces/make-autocomplete.ts +15 -0
  27. package/src/interfaces/make-button.ts +20 -0
  28. package/src/interfaces/make-field-group.ts +13 -0
  29. package/src/interfaces/make-field.ts +14 -0
  30. package/src/interfaces/make-multi-select.ts +14 -0
  31. package/src/interfaces/make-select.ts +14 -0
  32. package/src/interfaces/make-text.ts +12 -0
  33. package/src/interfaces/make-textarea.ts +11 -0
  34. package/src/interfaces/make-title.ts +3 -0
  35. package/src/interfaces/make-toggle.ts +9 -0
  36. package/src/interfaces/make-upload.ts +13 -0
  37. package/src/interfaces/modal.ts +50 -0
  38. package/src/interfaces/option.ts +4 -0
  39. package/src/interfaces/portal.ts +8 -0
  40. package/src/modal.tsx +168 -0
  41. package/src/tools/general.ts +6 -0
  42. package/tsconfig.json +13 -0
@@ -0,0 +1,45 @@
1
+ import React, { FC, useEffect } from 'react'
2
+ import { Controller } from 'react-hook-form'
3
+ import { useFieldRender } from '../../hooks/field-render'
4
+ import { generateId } from '../../tools/general'
5
+ import { IMakeTextareaProps } from '../../interfaces/make-textarea'
6
+ import { Textarea } from '@nextui-org/react'
7
+
8
+ const MakeTextarea: FC<IMakeTextareaProps> = ({ element, ...props }) => {
9
+ const { render, enable, checkField } = useFieldRender({ ...props, element })
10
+
11
+ useEffect(() => {
12
+ const subscription = props.watch((value, { name, type }) => checkField(value, { name, type }))
13
+ return () => subscription.unsubscribe()
14
+ }, [checkField, props, props.watch])
15
+
16
+ return (
17
+ render
18
+ ? <Controller
19
+ name={element.name}
20
+ control={props.control}
21
+ rules={{
22
+ required: element.validation.required,
23
+ pattern: {
24
+ value: element.validation.regex as RegExp ?? /(.*)/,
25
+ message: element.validation.message ?? ''
26
+ }
27
+ }}
28
+ render={({ field: { onChange, value }, fieldState: { invalid } }) => (
29
+ <Textarea
30
+ {...element}
31
+ disableAutosize
32
+ id={element.id ?? generateId()}
33
+ onChange={onChange}
34
+ label={element.label}
35
+ value={value ?? ''}
36
+ errorMessage={invalid ? (element.validation.message ?? '') : undefined}
37
+ disabled={element.disabled ?? !enable}
38
+ />
39
+ )}
40
+ />
41
+ : null
42
+ )
43
+ }
44
+
45
+ export default MakeTextarea
@@ -0,0 +1,12 @@
1
+ import React, { FC } from 'react'
2
+ import { IMakeTitle } from '../../interfaces/make-title'
3
+
4
+ const MakeTitle: FC<IMakeTitle> = ({ title }) => {
5
+ return (
6
+ <div className='flex text-center items-center justify-center text-xl'>
7
+ {title}
8
+ </div>
9
+ )
10
+ }
11
+
12
+ export default MakeTitle
@@ -0,0 +1,44 @@
1
+ import React, { FC, useEffect } from 'react'
2
+ import { Controller } from 'react-hook-form'
3
+ import { useFieldRender } from '../../hooks/field-render'
4
+ import { IMakeToggleProps } from '../../interfaces/make-toggle'
5
+ import { Switch } from '@nextui-org/react'
6
+ import { generateId } from '../../tools/general'
7
+
8
+ const MakeToggle: FC<IMakeToggleProps> = (props) => {
9
+ const { render, enable, checkField } = useFieldRender(props)
10
+
11
+ useEffect(() => {
12
+ const subscription = props.watch((value, { name, type }) => checkField(value, { name, type }))
13
+ return () => subscription.unsubscribe()
14
+ }, [checkField, props, props.watch])
15
+
16
+ return (
17
+ render
18
+ ? <Controller
19
+ control={props.control}
20
+ name={props.element.name}
21
+ rules={{
22
+ required: props.element.validation.required,
23
+ pattern: {
24
+ value: props.element.validation.regex as RegExp ?? /(.*)/,
25
+ message: props.element.validation.message ?? ''
26
+ }
27
+ }}
28
+ render={({ field: { onChange, value } }) => (
29
+ <Switch
30
+ {...props.element}
31
+ id={props.element.id ?? generateId()}
32
+ isSelected={value}
33
+ onValueChange={onChange}
34
+ isDisabled={props.element.disabled ?? !enable}
35
+ >
36
+ {props.element.label}
37
+ </Switch>
38
+ )}
39
+ />
40
+ : null
41
+ )
42
+ }
43
+
44
+ export default MakeToggle
@@ -0,0 +1,40 @@
1
+ import React,{ FC } from 'react'
2
+ import { Controller } from 'react-hook-form'
3
+ import InputUpload from '../input-upload/input-upload'
4
+ import { useFieldRender } from '../../hooks/field-render'
5
+ import { IMakeUploadProps } from '../../interfaces/make-upload'
6
+
7
+ const MakeUpload : FC<IMakeUploadProps> = (props) => {
8
+ const { render, enable } = useFieldRender(props)
9
+
10
+ return (
11
+ render
12
+ ? <Controller
13
+ control={props.control}
14
+ name={props.element.name}
15
+ rules={{
16
+ required: props.element.validation.required,
17
+ pattern: {
18
+ value: props.element.validation.regex as RegExp ?? /(.*)/,
19
+ message: props.element.validation.message ?? ''
20
+ }
21
+ }}
22
+ render={({ field: { onChange } }) => (
23
+ <InputUpload
24
+ id={props.element.id}
25
+ name={props.element.name}
26
+ disabled={props.element.disabled ?? !enable}
27
+ onChange={onChange}
28
+ label={props.element.label}
29
+ helpText={props.element.helpText}
30
+ style={props.element.styles}
31
+ accept={props.element.accept}
32
+ read={props.element.read}
33
+ />
34
+ )}
35
+ />
36
+ : null
37
+ )
38
+ }
39
+
40
+ export default MakeUpload
@@ -0,0 +1,36 @@
1
+ 'use client'
2
+ import React, { useRef, useEffect, useState, FC } from 'react'
3
+ import { createPortal } from 'react-dom'
4
+ import { IPortal } from '../../interfaces/portal'
5
+
6
+ export const Portal: FC<IPortal> = (props) => {
7
+ const ref = useRef<Element | null>(null)
8
+ const [mounted, setMounted] = useState(false)
9
+
10
+ useEffect(() => {
11
+ if (mounted && !props.portalOpen && props.closeTime > 0) {
12
+ const timeoutId = setTimeout(() => {
13
+ setMounted(false)
14
+ ref.current = null
15
+ }, props.closeTime)
16
+
17
+ return () => clearTimeout(timeoutId)
18
+ } else if (mounted && !props.portalOpen) {
19
+ setMounted(false)
20
+ ref.current = null
21
+ } else if (!mounted && props.portalOpen) {
22
+ ref.current = document.querySelector<HTMLElement>(props.portalTag ?? '#portal')
23
+ setMounted(true)
24
+ }
25
+ }, [mounted, props.closeTime, props.portalOpen, props.portalTag])
26
+
27
+ return (
28
+ mounted && ref.current
29
+ ? createPortal(
30
+ <div className='transition-all delay-100 fixed top-0 left-0 w-full h-full grid place-items-center bg-black bg-opacity-40 z-20'>
31
+ {props.children}
32
+ </div>
33
+ , ref.current)
34
+ : null
35
+ )
36
+ }
@@ -0,0 +1,104 @@
1
+ 'use client'
2
+ import { useCallback, useMemo, useState } from 'react'
3
+ import { IField, IFieldProps } from '../interfaces/field'
4
+ import { IOption } from '../interfaces/option'
5
+ import { IModalLiveDataCondition, IModalRenderCondition } from '../interfaces/modal'
6
+
7
+ export type IFormData = Record<string, unknown>
8
+
9
+ export interface IWatchEvent {
10
+ name: string | undefined;
11
+ type: string | undefined;
12
+ }
13
+
14
+ export interface IFieldRender {
15
+ render: boolean | null;
16
+ enable: boolean | null;
17
+ checkField: (formData: IFormData, { name, type }: IWatchEvent) => void;
18
+ liveData?: Array<IOption>;
19
+ liveSearching?: boolean;
20
+ }
21
+
22
+ export interface IFieldRenderProps extends Pick<IFieldProps, 'setValue'> {
23
+ element: Partial<Pick<IField, 'enableIf'|'renderIf'|'name' >> & Partial<Record<'liveData', IModalLiveDataCondition>>
24
+ }
25
+
26
+ export const useFieldRender = (props: IFieldRenderProps): IFieldRender => {
27
+ const [render, setRender] = useState<boolean|null>(null)
28
+ const [enable, setEnable] = useState<boolean|null>(null)
29
+ const [liveSearching, setLiveSearching] = useState<boolean>(false)
30
+ const [liveData, setLiveData] = useState<Array<IOption> | undefined>(undefined)
31
+
32
+ const renderCondition = useMemo<boolean>(
33
+ () => {
34
+ const isRender: boolean = props.element.renderIf !== undefined
35
+ if (render === null) setRender(!isRender)
36
+ return isRender
37
+ }, [props.element.renderIf, render]
38
+ )
39
+
40
+ const enableCondition = useMemo<boolean>(
41
+ () => {
42
+ const isEnable: boolean = props.element.enableIf !== undefined
43
+ if (enable === null) setEnable(!isEnable)
44
+ return isEnable
45
+ }, [props.element.enableIf, enable]
46
+ )
47
+
48
+ const liveDataCondition = useMemo<boolean>(
49
+ () => {
50
+ return props.element.liveData !== undefined
51
+ }, [props.element.liveData]
52
+ )
53
+
54
+ const renderConditionList: string[] = useMemo(() => {
55
+ return renderCondition ? Object.keys(props.element.renderIf as IModalRenderCondition) : []
56
+ }, [props.element.renderIf, renderCondition])
57
+
58
+ const enableConditionList: string[] = useMemo(() => {
59
+ return enableCondition ? Object.keys(props.element.enableIf as IModalRenderCondition) : []
60
+ }, [enableCondition, props.element.enableIf])
61
+
62
+ const liveDataAction = useCallback(
63
+ async (field: string |number | Array<unknown>, formData: IFormData) => {
64
+ if (typeof field === 'string' && props.element.liveData?.action) {
65
+ const options = props.element.liveData.action(field, formData)
66
+ return options ?? []
67
+ }
68
+ return [] as Array<IOption>
69
+ }, [props.element.liveData]
70
+ )
71
+
72
+ const checkField = useCallback(
73
+ async (formData: IFormData, { name }: IWatchEvent) => {
74
+ const key: string = name ?? ''
75
+ const targetField = formData[key] as string | number
76
+
77
+ if (renderCondition && renderConditionList.includes(key)) {
78
+ const renderStatus: boolean = (props.element.renderIf as IModalRenderCondition)[key].includes(targetField)
79
+ if (render !== renderStatus) setRender(renderStatus)
80
+ } else if (enableCondition && enableConditionList.includes(key)) {
81
+ const enableStatus: boolean = (props.element.enableIf as IModalRenderCondition)[key].includes(targetField)
82
+ if (enable !== enableStatus) setEnable(enableStatus)
83
+ }
84
+
85
+ if (liveDataCondition && key === props.element.liveData?.condition) {
86
+ if (targetField) {
87
+ setLiveSearching(true)
88
+ const options: Array<IOption> = await liveDataAction(targetField, formData)
89
+ if (liveData && JSON.stringify(liveData) !== JSON.stringify(options)) { props.setValue(props.element.name as string, options) }
90
+ setLiveData(options)
91
+ setLiveSearching(false)
92
+ }
93
+ }
94
+ }, [enable, enableCondition, enableConditionList, liveData, liveDataAction, liveDataCondition, props, render, renderCondition, renderConditionList]
95
+ )
96
+
97
+ return {
98
+ render,
99
+ enable,
100
+ checkField,
101
+ liveData,
102
+ liveSearching
103
+ }
104
+ }
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+ import { useState } from 'react'
3
+ import { IModal, IModalConfigProps } from '../interfaces/modal'
4
+
5
+ interface IModalHandler {
6
+ openModal: (config: IModalConfigProps) => void
7
+ modalProps: IModal
8
+ }
9
+
10
+ export function useModalHandler (): IModalHandler {
11
+ const initialState: IModalHandler = {
12
+ modalProps: {
13
+ config: {} as IModalConfigProps,
14
+ close: () => {},
15
+ open: false,
16
+ },
17
+ openModal: () => {},
18
+ }
19
+
20
+ const [modalConfig, setModalConfig] = useState<IModalHandler>(initialState)
21
+
22
+ const openModal = (config: IModalConfigProps) => {
23
+ if (!config) {
24
+ alert(`¡WARNING! this modal was not returned config, please check before use`)
25
+ return
26
+ }
27
+
28
+ setModalConfig({ openModal, modalProps: { config, open: true, close } })
29
+ }
30
+
31
+ const closeModal = () => setModalConfig(initialState)
32
+
33
+ return {
34
+ openModal,
35
+ modalProps: { ...modalConfig.modalProps, close: closeModal },
36
+ }
37
+ }
@@ -0,0 +1,30 @@
1
+ import { CSSProperties } from 'react'
2
+ import { Control, FieldValues, UseFormSetValue, UseFormWatch } from 'react-hook-form'
3
+ import { IOption } from './option'
4
+
5
+ export interface IFieldLiveData {
6
+ action: (data: string, ...args: unknown[]) => Promise<Array<IOption>>;
7
+ condition: string
8
+ }
9
+
10
+ export interface IField {
11
+ name: string
12
+ id?: string
13
+ label?: string
14
+ styles?: CSSProperties
15
+ defaultValue?: string
16
+ renderIf?: Record<string, Array<string | number>>
17
+ enableIf?: Record<string, Array<string | number>>
18
+ validation: {
19
+ required: boolean
20
+ regex?: RegExp
21
+ message?: string
22
+ }
23
+ disabled?: boolean
24
+ }
25
+
26
+ export interface IFieldProps {
27
+ control: Control<FieldValues, unknown>;
28
+ watch: UseFormWatch<FieldValues>;
29
+ setValue: UseFormSetValue<FieldValues>
30
+ }
@@ -0,0 +1,36 @@
1
+ import { ChangeEvent, CSSProperties } from 'react'
2
+
3
+ export interface IFileResult {
4
+ name: string;
5
+ size: number;
6
+ data: string
7
+ }
8
+
9
+
10
+ export interface IFile {
11
+ data?: string
12
+ name: string;
13
+ size: number;
14
+ }
15
+ export interface IInputState {
16
+ state: boolean;
17
+ file: IFile
18
+ }
19
+
20
+ export interface IInputStorage {
21
+ setData: (file: IInputState) => void;
22
+ clearFuntion: () => void;
23
+ }
24
+ export interface IInputUpload {
25
+ id?: string;
26
+ value?: string;
27
+ onChange: (event: ChangeEvent<HTMLInputElement> | IFileResult | FileList | null) => void;
28
+ accept?:string;
29
+ label?: string
30
+ helpText?: string
31
+ clearfunction?: () => void;
32
+ style?:CSSProperties;
33
+ name: string;
34
+ disabled?: boolean;
35
+ read?: boolean;
36
+ }
@@ -0,0 +1,15 @@
1
+ import { IField, IFieldProps } from './field'
2
+ import { IModalLiveDataCondition } from './modal'
3
+ import { IOption } from './option'
4
+
5
+ export interface IMakeAutoComplete extends IField {
6
+ elementType: 'autocomplete'
7
+ options: Array<IOption>
8
+ defaultOption?: boolean
9
+ defaultOptionName?: string
10
+ liveData?: IModalLiveDataCondition
11
+ }
12
+
13
+ export interface IMakeAutoCompleteProps extends IFieldProps {
14
+ element: IMakeAutoComplete
15
+ }
@@ -0,0 +1,20 @@
1
+ import { CSSProperties } from 'react'
2
+ import { IFieldProps } from './field'
3
+
4
+ export interface IMakeButton {
5
+ elementType: 'button';
6
+ disabled?: boolean
7
+ className?: string;
8
+ variant?: 'solid' | 'light' | 'flat' | 'bordered' | 'faded' | 'shadow' | 'ghost'
9
+ text?: string;
10
+ type?: 'button' | 'submit' | 'reset';
11
+ onClick?: () => void;
12
+ iconName?: string;
13
+ iconSize?: number | string;
14
+ iconClassName?: CSSProperties;
15
+ color?: 'primary' | 'secondary' | 'default' | 'success' | 'warning' | 'danger'
16
+ }
17
+
18
+ export interface IMakeButtonProps extends IFieldProps {
19
+ element: IMakeButton
20
+ }
@@ -0,0 +1,13 @@
1
+ import { CSSProperties } from 'react'
2
+ import { IFieldProps } from './field'
3
+ import { IModalField } from './modal'
4
+
5
+ export interface IMakeFieldGroup {
6
+ elementType: 'group';
7
+ groups: Array<IModalField>;
8
+ style?: CSSProperties;
9
+ }
10
+
11
+ export interface IMakeFieldGroupProps extends IFieldProps {
12
+ element: IMakeFieldGroup;
13
+ }
@@ -0,0 +1,14 @@
1
+ import { HTMLInputTypeAttribute } from 'react'
2
+ import { IField, IFieldProps } from './field'
3
+
4
+ export interface IMakeInput extends IField {
5
+ elementType: 'input'
6
+ placeHolder?: string
7
+ min?: string
8
+ max?: string
9
+ type?: HTMLInputTypeAttribute
10
+ }
11
+
12
+ export interface IMakeInputProps extends IFieldProps {
13
+ element: IMakeInput
14
+ }
@@ -0,0 +1,14 @@
1
+ import { IOption } from './option'
2
+ import { IField, IFieldProps } from './field'
3
+ import { IModalLiveDataCondition } from './modal'
4
+
5
+ export interface IMakeMultiSelect extends Omit<IField, 'defaultValue'> {
6
+ elementType: 'multiselect';
7
+ options: Array<IOption>;
8
+ liveData?: IModalLiveDataCondition;
9
+ defaultValue?: Array<string>
10
+ }
11
+
12
+ export interface IMakeMultiSelectProps extends IFieldProps {
13
+ element: IMakeMultiSelect
14
+ }
@@ -0,0 +1,14 @@
1
+ import { IField, IFieldProps } from './field'
2
+ import { IModalLiveDataCondition } from './modal'
3
+
4
+ export interface IMakeSelect extends IField {
5
+ elementType: 'select'
6
+ options: Array<Record<'id'|'name', string>>
7
+ defaultOption?: boolean
8
+ defaultOptionName?: string
9
+ liveData?: IModalLiveDataCondition
10
+ }
11
+
12
+ export interface IMakeSelectProps extends IFieldProps {
13
+ element: IMakeSelect
14
+ }
@@ -0,0 +1,12 @@
1
+ import { CSSProperties } from 'react'
2
+ import { IFieldProps } from './field'
3
+
4
+ export interface IMakeText {
5
+ elementType: 'text'
6
+ text: string
7
+ styles?: CSSProperties
8
+ }
9
+
10
+ export interface IMakeTextProps extends IFieldProps {
11
+ element: IMakeText
12
+ }
@@ -0,0 +1,11 @@
1
+ import { IField, IFieldProps } from './field'
2
+
3
+ export interface IMakeTextarea extends IField {
4
+ elementType: 'textarea'
5
+ cols?: number
6
+ rows?: number
7
+ }
8
+
9
+ export interface IMakeTextareaProps extends IFieldProps {
10
+ element: IMakeTextarea
11
+ }
@@ -0,0 +1,3 @@
1
+ export interface IMakeTitle {
2
+ title: string
3
+ }
@@ -0,0 +1,9 @@
1
+ import { IField, IFieldProps } from './field'
2
+
3
+ export interface IMakeToggle extends IField{
4
+ elementType: 'toggle'
5
+ }
6
+
7
+ export interface IMakeToggleProps extends IFieldProps {
8
+ element: IMakeToggle
9
+ }
@@ -0,0 +1,13 @@
1
+ import { IField, IFieldProps } from './field'
2
+
3
+ export interface IMakeUpload extends Omit<IField, 'defaultValue'> {
4
+ elementType: 'upload';
5
+ helpText?: string;
6
+ read: boolean;
7
+ image?: boolean;
8
+ accept?:string;
9
+ }
10
+
11
+ export interface IMakeUploadProps extends IFieldProps {
12
+ element: IMakeUpload
13
+ }
@@ -0,0 +1,50 @@
1
+ import { CSSProperties } from 'react'
2
+ import { IMakeSelect } from './make-select'
3
+ import { IMakeInput } from './make-field'
4
+ import { IMakeTextarea } from './make-textarea'
5
+ import { IMakeToggle } from './make-toggle'
6
+ import { IMakeMultiSelect } from './make-multi-select'
7
+ import { IMakeText } from './make-text'
8
+ import { IMakeFieldGroup } from './make-field-group'
9
+ import { IMakeUpload } from './make-upload'
10
+ import { IOption } from './option'
11
+ import { IMakeButton } from './make-button'
12
+ import { IMakeAutoComplete } from './make-autocomplete'
13
+
14
+ export type IModalField = IMakeSelect | IMakeInput | IMakeFieldGroup | IMakeTextarea | IMakeToggle | IMakeMultiSelect | IMakeText | IMakeUpload | IMakeButton | IMakeAutoComplete
15
+
16
+ export type IFormField = IMakeSelect | IMakeInput | IMakeTextarea | IMakeToggle | IMakeMultiSelect
17
+
18
+ export type IModalRenderCondition = Record<string, Array<string | number>>
19
+
20
+ export type IModalLiveDataCondition = {
21
+ action: (data: string, ...args: never[]) => Promise<Array<IOption>>;
22
+ condition: string
23
+ }
24
+
25
+ export interface IModalConfigProps {
26
+ fields: Array<IModalField>
27
+ title: string
28
+ action: {
29
+ name: string
30
+ action?: (data: never) => void
31
+ hide?: boolean
32
+ }
33
+ cancel?: {
34
+ name?: string;
35
+ action?: () => void
36
+ hide?: boolean;
37
+ }
38
+ reservedData?: Record<string, string | number>
39
+ styles?: CSSProperties;
40
+ overFlowBody?: string | number
41
+ minHeightBody?: string | number
42
+ }
43
+
44
+ export type IModalConfigLoader<T = never, D = never> = (props: T, action: (modalResult: D) => void) => IModalConfigProps
45
+
46
+ export interface IModal {
47
+ open: boolean;
48
+ close: () => void
49
+ config: IModalConfigProps
50
+ }
@@ -0,0 +1,4 @@
1
+ export interface IOption {
2
+ id: string;
3
+ name: string;
4
+ }
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export interface IPortal {
4
+ children: ReactNode;
5
+ closeTime: number;
6
+ portalOpen: boolean;
7
+ portalTag?: string;
8
+ }