Fixed linting issues, added container

This commit is contained in:
Hugo Bernier 2023-05-16 17:56:00 -04:00
parent 2beeb06fc7
commit 034937a0fe
10 changed files with 147 additions and 60 deletions

View File

@ -0,0 +1,39 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.17.1",
"image": "docker.io/m365pnp/spfx:1.17.1",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4321,
35729
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
// Not needed for SPFx>= 1.12.1
// "5432": {
// "protocol": "https",
// "label": "Workbench",
// "onAutoForward": "silent"
// },
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -0,0 +1,33 @@
echo
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
npm install
## commands to create dev certificate and copy it to the root folder of the project
echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore
then
echo "# .CER Certificates" >> .gitignore
echo "*.cer" >> .gitignore
fi
## add *.pem to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.pem' ./.gitignore
then
echo "# .PEM Certificates" >> .gitignore
echo "*.pem" >> .gitignore
fi
echo
echo -e "\e[1;92mReady!\e[0m"
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default function useObject<T>(InitialValue?: T) {
const [value, setValue] = useState<T>(InitialValue ?? {} as T);
const updateValue: (Updates: Partial<T>) => void = (Updates: Partial<T>) => setValue((prev) => ({ ...prev, ...Updates }))

View File

@ -2,5 +2,6 @@ import { IForm } from "./Form";
export interface SaveObject {
form: IForm;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response: any;
}

View File

@ -13,11 +13,11 @@ const UNSUPPORTED_LOOKUP_FIELDTYPES: FieldType[] = [FieldType.PlaceHolder, Field
export const GetLookupFields: (Fields: IField[]) => IField[] = (Fields: IField[]) => {
const arr: IField[] = [];
for (let field of Fields) {
if (field.Type == FieldType.FieldGroup) {
for (const field of Fields) {
if (field.Type === FieldType.FieldGroup) {
arr.push(...GetLookupFields((field as IGroupField).Fields))
} else if (field.Type == FieldType.Conditional) {
if ((field as IConditionalField).Field.Type == FieldType.FieldGroup) {
} else if (field.Type === FieldType.Conditional) {
if ((field as IConditionalField).Field.Type === FieldType.FieldGroup) {
arr.push(...GetLookupFields(((field as IConditionalField).Field as IGroupField).Fields))
} else {
arr.push((field as IConditionalField).Field);
@ -27,5 +27,5 @@ export const GetLookupFields: (Fields: IField[]) => IField[] = (Fields: IField[]
}
}
return arr.filter(x => x.DisplayName != null && !UNSUPPORTED_LOOKUP_FIELDTYPES.some(bannedType => bannedType == x.Type));;
return arr.filter(x => x.DisplayName !== null && !UNSUPPORTED_LOOKUP_FIELDTYPES.some(bannedType => bannedType === x.Type));
}

View File

@ -41,6 +41,7 @@ export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebP
JsonForm,
{
Form: JSON.parse(this.properties.formJson),
// eslint-disable-next-line no-return-assign
SaveForm: (updated: IForm) => this.properties.formJson = JSON.stringify({ ...JSON.parse(this.properties.formJson), ...updated }, null, 2),
Mode: this.displayMode,
ListId: this.properties.listId,

View File

@ -6,6 +6,7 @@ import { Label } from 'office-ui-fabric-react';
export interface IFieldProps {
onChange: (updates: object) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: any;
field: IField;
readonly: boolean;
@ -50,6 +51,7 @@ export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWi
/>;
case FieldType.Choice: return <Dropdown
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options={(field as any as IChoiceField).Options.map(x => ({ key: x, text: x }))}
selectedKey={form[field.Id] ?? ""}
label={field.DisplayName}
@ -57,32 +59,34 @@ export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWi
/>
case FieldType.MultiChoice: return <Dropdown
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options={(field as any as IChoiceField).Options.map(x => ({ key: x, text: x }))}
selectedKeys={form[field.Id] ?? []}
label={field.DisplayName}
multiSelect
onChange={readonly ? null : (_, val) => {
let selected: string[] = form[field.Id] ?? [];
selected = val.selected ? [...selected, val.key as string] : selected.filter(x => x != val.key);
selected = val.selected ? [...selected, val.key as string] : selected.filter(x => x !== val.key);
onChange({ [field.Id]: selected })
}}
/>
case FieldType.FieldGroup: return <div>
{(field.DisplayName != null || field.DisplayName != "") && <Label>{field.DisplayName}</Label>}
{(field.DisplayName !== null || field.DisplayName !== "") && <Label>{field.DisplayName}</Label>}
<div
style={{
display: "grid",
gridTemplateColumns: (field as IGroupField).Direction == GroupDirection.Horizontal ? `repeat(auto-fill,minmax(calc(${100 / (field as any as IGroupField).Fields.length}% - 10px),1fr))` : '',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
gridTemplateColumns: (field as IGroupField).Direction === GroupDirection.Horizontal ? `repeat(auto-fill,minmax(calc(${100 / (field as any as IGroupField).Fields.length}% - 10px),1fr))` : '',
gap: 10
}}>
{(field as IGroupField).Fields.map(f => <Field {...props} field={f} />)}
{(field as IGroupField).Fields.map((f, index) => <Field {...props} field={f} key={index} />)}
</div>
</div>;
case FieldType.Conditional: {
const f = (field as IConditionalField);
const visible = form[f.LookupFieldId] == f.MatchValue
const visible = form[f.LookupFieldId] === f.MatchValue
if (!visible) return <></>
return <Field readonly={readonly} field={f.Field} form={form} onChange={onChange} />
}

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import * as React from 'react';
import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../../../Models/FormField';
import { ActionButton, ChoiceGroup, DefaultButton, Dialog, DialogFooter, Dropdown, Label, Position, PrimaryButton, SpinButton, Stack, TextField } from '@fluentui/react';
@ -42,16 +43,16 @@ export const FieldEditorDialog: React.FunctionComponent<IFieldEditorDialogProps>
const t = val.key as FieldType;
const updates: Partial<IField | IChoiceField | IGroupField | IConditionalField> = { Type: t }
if (t == FieldType.Choice || t == FieldType.MultiChoice)
if ((field as IChoiceField).Options == null)
if (t === FieldType.Choice || t === FieldType.MultiChoice)
if ((field as IChoiceField).Options === null)
(updates as Partial<IChoiceField>).Options = [];
if (t == FieldType.Conditional)
if ((field as IConditionalField).Field == null)
if (t === FieldType.Conditional)
if ((field as IConditionalField).Field === null)
(updates as Partial<IConditionalField>).Field = NewField();
if (t == FieldType.FieldGroup)
if ((field as IGroupField).Fields == null) {
if (t === FieldType.FieldGroup)
if ((field as IGroupField).Fields === null) {
(updates as Partial<IGroupField>).Fields = [];
(updates as Partial<IGroupField>).Direction = GroupDirection.Horizontal;
}
@ -60,12 +61,12 @@ export const FieldEditorDialog: React.FunctionComponent<IFieldEditorDialogProps>
}}
/>
{FieldType.Conditional != field.Type && <TextField label='Title' value={field.DisplayName} onChange={(_, val) => updateValue({ DisplayName: val })} />}
{FieldType.Conditional !== field.Type && <TextField label='Title' value={field.DisplayName} onChange={(_, val) => updateValue({ DisplayName: val })} />}
{[FieldType.Choice, FieldType.MultiChoice].some(x => x == field.Type) && <ChoiceFieldOptions field={field as IChoiceField} updateValue={updateValue} />}
{FieldType.Conditional == field.Type && <ConditionalFieldOptions field={field as IConditionalField} updateValue={updateValue} allFieldsFlat={props.allFieldsFlat} />}
{FieldType.FieldGroup == field.Type && <GroupFieldOptions field={field as IGroupField} updateValue={updateValue} />}
{[FieldType.Choice, FieldType.MultiChoice].some(x => x === field.Type) && <ChoiceFieldOptions field={field as IChoiceField} updateValue={updateValue} />}
{FieldType.Conditional === field.Type && <ConditionalFieldOptions field={field as IConditionalField} updateValue={updateValue} allFieldsFlat={props.allFieldsFlat} />}
{FieldType.FieldGroup === field.Type && <GroupFieldOptions field={field as IGroupField} updateValue={updateValue} />}
<DialogFooter>
<PrimaryButton text='Delete' iconProps={{ iconName: 'delete' }} onClick={() => props.delete()} styles={{ root: { backgroundColor: "#FF0000" }, rootHovered: { backgroundColor: "#D10000" }, rootChecked: { backgroundColor: "#A30000" } }} />
@ -90,9 +91,12 @@ export const GroupFieldOptions: React.FunctionComponent<IGroupFieldOptionsProps>
label='Direction'
selectedKey={field.Direction}
options={[
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ key: GroupDirection.Horizontal as any as string, text: "Horizontal", iconProps: { iconName: "AlignVerticalCenter" } },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ key: GroupDirection.Vertical as any as string, text: "Vertical", iconProps: { iconName: "AlignHorizontalCenter" } }
]}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange={(_, val) => updateValue({ Direction: val.key as any as GroupDirection })}
/>
</>
@ -110,7 +114,7 @@ const ChoiceFieldOptions: React.FunctionComponent<IChoiceFieldOptionsProps> = (p
return (<>
<Label>Options</Label>
{(field as IChoiceField).Options.map((val, index) => {
return <div style={{ display: "flex" }}>
return <div style={{ display: "flex" }} key={index}>
<TextField
styles={{ root: { flexGrow: 1 } }}
value={val} onChange={(_, val) => {
@ -118,7 +122,7 @@ const ChoiceFieldOptions: React.FunctionComponent<IChoiceFieldOptionsProps> = (p
options[index] = val
updateValue({ Options: options });
}} />
<ActionButton iconProps={{ iconName: "Delete" }} onClick={() => updateValue({ Options: (field as IChoiceField).Options.filter((_, i) => i != index) })} />
<ActionButton iconProps={{ iconName: "Delete" }} onClick={() => updateValue({ Options: (field as IChoiceField).Options.filter((_, i) => i !== index) })} />
</div>
})}
<PrimaryButton iconProps={{ iconName: "Add" }} text='Add option' onClick={() => {
@ -136,7 +140,7 @@ interface IConditionalFieldOptionsProps {
const ConditionalFieldOptions: React.FunctionComponent<IConditionalFieldOptionsProps> = (props: React.PropsWithChildren<IConditionalFieldOptionsProps>) => {
const { allFieldsFlat, field, updateValue } = props
const targetField = allFieldsFlat.filter(x => x.Id == (field as IConditionalField).LookupFieldId)[0]
const targetField = allFieldsFlat.filter(x => x.Id === (field as IConditionalField).LookupFieldId)[0]
return (
<>
@ -147,23 +151,23 @@ const ConditionalFieldOptions: React.FunctionComponent<IConditionalFieldOptionsP
onChange={(_, val) => updateValue({ LookupFieldId: val.key as string })}
/>
{targetField != null &&
{targetField !== null &&
<>
{FieldType.Choice == targetField.Type && <Dropdown
{FieldType.Choice === targetField.Type && <Dropdown
label='Show if is equal to'
options={(targetField as IChoiceField).Options.map(x => ({ key: x, text: x }))}
selectedKey={(field as IConditionalField).MatchValue as string}
onChange={(_, val) => updateValue({ MatchValue: val.text })}
/>}
{FieldType.Boolean == targetField.Type && <Dropdown
{FieldType.Boolean === targetField.Type && <Dropdown
label='Show if is equal to'
options={[{ key: true.toString(), text: "Yes" }, { key: false.toString(), text: "No" }]}
selectedKey={(field as IConditionalField).MatchValue?.toString()}
onChange={(_, val) => updateValue({ MatchValue: val.key == true.toString() })}
onChange={(_, val) => updateValue({ MatchValue: val.key === true.toString() })}
/>}
{FieldType.Number == targetField.Type && <SpinButton
{FieldType.Number === targetField.Type && <SpinButton
label='Show if is equal to'
inputMode='numeric'
labelPosition={Position.top}
@ -172,7 +176,7 @@ const ConditionalFieldOptions: React.FunctionComponent<IConditionalFieldOptionsP
/>
}
{[FieldType.Text, FieldType.MultilineText].some(x => x == targetField.Type) && <TextField
{[FieldType.Text, FieldType.MultilineText].some(x => x === targetField.Type) && <TextField
value={(field as IConditionalField).MatchValue as string}
label={"Value to look for"}
onChange={(_, val) => updateValue({ MatchValue: val })}

View File

@ -17,7 +17,7 @@ const MAX_NUMBER_OF_ITEMS_IN_GROUP: number = 5;
export const FormFieldCustomizer: React.FunctionComponent<IFormFieldCustomizer> = (props: React.PropsWithChildren<IFormFieldCustomizer>) => {
const { field } = props;
const [shouldEdit, setShouldEdit] = React.useState<Boolean>(false);
const [shouldEdit, setShouldEdit] = React.useState<boolean>(false);
const editDialog = <FieldEditorDialog
allFieldsFlat={props.allFieldsFlat}
@ -34,20 +34,21 @@ export const FormFieldCustomizer: React.FunctionComponent<IFormFieldCustomizer>
/>
if (FieldType.FieldGroup == field.Type) {
if (FieldType.FieldGroup === field.Type) {
const f = (field as IGroupField)
const AtCapacity = f.Fields.length == MAX_NUMBER_OF_ITEMS_IN_GROUP;
const AtCapacity = f.Fields.length === MAX_NUMBER_OF_ITEMS_IN_GROUP;
return <div>
{shouldEdit && editDialog}
<div style={{ display: "flex" }}>
{(f.DisplayName != null || f.DisplayName != "") && <Label disabled>{f.DisplayName}</Label>}
{(f.DisplayName !== null || f.DisplayName !== "") && <Label disabled>{f.DisplayName}</Label>}
<ActionButton iconProps={{ iconName: "Edit" }} onClick={() => setShouldEdit(true)} />
<ActionButton iconProps={{ iconName: "Delete" }} onClick={() => props.delete()} />
</div>
<div
style={{
display: "grid",
gridTemplateColumns: (field as IGroupField).Direction == GroupDirection.Horizontal ? `repeat(auto-fill,minmax(calc(${100 / ((field as any as IGroupField).Fields.length + (!AtCapacity ? 1 : 0))}% - 10px),1fr))` : '',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
gridTemplateColumns: (field as IGroupField).Direction === GroupDirection.Horizontal ? `repeat(auto-fill,minmax(calc(${100 / ((field as any as IGroupField).Fields.length + (!AtCapacity ? 1 : 0))}% - 10px),1fr))` : '',
gap: 10
}}>
{f.Fields.map((child, index) => {
@ -55,9 +56,9 @@ export const FormFieldCustomizer: React.FunctionComponent<IFormFieldCustomizer>
key={child.Id}
allFieldsFlat={props.allFieldsFlat}
field={child}
delete={() => props.update({ Fields: (field as IGroupField).Fields.filter(x => x.Id != child.Id) })}
delete={() => props.update({ Fields: (field as IGroupField).Fields.filter(x => x.Id !== child.Id) })}
update={(val) => {
let children = cloneDeep(f.Fields)
const children = cloneDeep(f.Fields)
children[index] = { ...children[index], ...val };
props.update({ Fields: children });
}}
@ -68,14 +69,14 @@ export const FormFieldCustomizer: React.FunctionComponent<IFormFieldCustomizer>
</div>;
}
if (FieldType.Conditional == field.Type) {
if (FieldType.Conditional === field.Type) {
const f = (field as IConditionalField);
const lookupField = props.allFieldsFlat.filter(x => x.Id == f.LookupFieldId)[0];
const lookupField = props.allFieldsFlat.filter(x => x.Id === f.LookupFieldId)[0];
return <>
{shouldEdit && editDialog}
<div style={{ border: `1px solid ${getTheme().palette.themeDarkAlt}` }}>
<div style={{ display: "flex", background: getTheme().palette.themeLighter, alignItems: 'center', paddingLeft: "1em" }}>
<Label>Visible if '{lookupField?.DisplayName}' is equal to '{f.MatchValue?.toString()}'</Label>
<Label>Visible if &apos;{lookupField?.DisplayName}&apos; is equal to &apos;{f.MatchValue?.toString()}&apos;</Label>
<ActionButton iconProps={{ iconName: "Edit" }} onClick={() => setShouldEdit(true)} />
<ActionButton iconProps={{ iconName: "Delete" }} onClick={() => props.delete()} />
</div>
@ -111,15 +112,15 @@ export const FormFieldCustomizer: React.FunctionComponent<IFormFieldCustomizer>
{shouldEdit && editDialog}
<div className={styles.EditField} onClick={() => setShouldEdit(true)}>
{FieldType.Label == field.Type && <Label styles={{ root: { cursor: "pointer" } }} disabled>{field.DisplayName}</Label>}
{FieldType.Header == field.Type && <Text variant='xLarge'>{field.DisplayName}</Text>}
{FieldType.Text == field.Type && <TextField {...genericProps} />}
{FieldType.MultilineText == field.Type && <TextField {...genericProps} rows={5} multiline />}
{FieldType.Number == field.Type && <SpinButton {...genericProps} inputMode='numeric' labelPosition={Position.top} />}
{FieldType.Boolean == field.Type && <Checkbox {...genericProps} styles={{ root: { marginTop: "2.5em" } }} />}
{FieldType.Choice == field.Type && <Dropdown {...genericProps} options={[]} />}
{FieldType.MultiChoice == field.Type && <Dropdown {...genericProps} options={[]} multiSelect />}
{FieldType.PlaceHolder == field.Type && <MessageBar messageBarType={MessageBarType.info} styles={{ root: { marginTop: "2.1em" } }}>Press here to setup the field!</MessageBar>}
{FieldType.Label === field.Type && <Label styles={{ root: { cursor: "pointer" } }} disabled>{field.DisplayName}</Label>}
{FieldType.Header === field.Type && <Text variant='xLarge'>{field.DisplayName}</Text>}
{FieldType.Text === field.Type && <TextField {...genericProps} />}
{FieldType.MultilineText === field.Type && <TextField {...genericProps} rows={5} multiline />}
{FieldType.Number === field.Type && <SpinButton {...genericProps} inputMode='numeric' labelPosition={Position.top} />}
{FieldType.Boolean === field.Type && <Checkbox {...genericProps} styles={{ root: { marginTop: "2.5em" } }} />}
{FieldType.Choice === field.Type && <Dropdown {...genericProps} options={[]} />}
{FieldType.MultiChoice === field.Type && <Dropdown {...genericProps} options={[]} multiSelect />}
{FieldType.PlaceHolder === field.Type && <MessageBar messageBarType={MessageBarType.info} styles={{ root: { marginTop: "2.1em" } }}>Press here to setup the field!</MessageBar>}
</div>
</span>

View File

@ -21,42 +21,44 @@ export interface IJsonFormProps {
export const JsonForm: React.FunctionComponent<IJsonFormProps> = (props: React.PropsWithChildren<IJsonFormProps>) => {
const { Mode, ServerRelativeUrl } = props;
const { value: Form, updateValue: UpdateForm, overwriteData: __SETFORM } = useObject<IForm>(props.ServerRelativeUrl ? { Fields: [], Title: "" } : props.Form);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { value: filledForm, updateValue, overwriteData: __SETFILLEDFORM } = useObject<any>();
const { provider } = React.useContext(SPFxContext);
React.useEffect(() => {
if (ServerRelativeUrl != null) {
const fetch = async () => {
if (ServerRelativeUrl !== null) {
const fetch = async (): Promise<void> => {
const result = await provider.GetSubmission(ServerRelativeUrl);
__SETFILLEDFORM(result.response);
__SETFORM(result.form);
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
fetch();
}
}, [])
const saveForm = async () => {
const saveForm = async (): Promise<void> => {
const serverRelativeUrl = await provider.SaveSubmission({ form: Form, response: filledForm });
var searchParams = new URLSearchParams(window.location.search);
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(FILLED_FORM_QUERY_KEY, serverRelativeUrl);
window.location.search = searchParams.toString();
}
if (props.ListId == null || props.ListId == "")
if (props.ListId === null || props.ListId === "")
return <Placeholder description={'Open the property pane and select a list to store responses'} iconName={'Edit'} iconText={'Please configure web part'} />
return (
<>
<WebPartTitle displayMode={Mode} title={Form.Title} updateProperty={(val) => UpdateForm({ Title: val })} />
{Mode == DisplayMode.Read &&
{Mode === DisplayMode.Read &&
<>
<Stack tokens={{ childrenGap: 5 }}>
{Form.Fields.map(field => {
return <Field readonly={props.ServerRelativeUrl != null} field={field} onChange={updateValue} form={filledForm} />
{Form.Fields.map((field, index) => {
return <Field readonly={props.ServerRelativeUrl !== null} field={field} onChange={updateValue} form={filledForm} key={index} />
})}
</Stack>
{props.ServerRelativeUrl == null &&
{props.ServerRelativeUrl === null &&
<DialogFooter>
<PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => saveForm()} />
</DialogFooter>
@ -64,19 +66,20 @@ export const JsonForm: React.FunctionComponent<IJsonFormProps> = (props: React.P
</>
}
{Mode == DisplayMode.Edit &&
{Mode === DisplayMode.Edit &&
<>
<Stack tokens={{ childrenGap: 5 }}>
{Form.Fields.map((Field, index) => {
return <FormFieldCustomizer
key={index}
allFieldsFlat={GetLookupFields(Form.Fields)}
field={Field}
delete={() => {
let fields = cloneDeep(Form.Fields).filter((x, i) => index != i);
const fields = cloneDeep(Form.Fields).filter((x, i) => index !== i);
UpdateForm({ Fields: fields });
}}
update={(val) => {
let fields = cloneDeep(Form.Fields)
const fields = cloneDeep(Form.Fields)
fields[index] = { ...fields[index], ...val };
UpdateForm({ Fields: fields });
}}