diff --git a/samples/react-json-form/.devcontainer/devcontainer.json b/samples/react-json-form/.devcontainer/devcontainer.json new file mode 100644 index 000000000..ea42caa43 --- /dev/null +++ b/samples/react-json-form/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/samples/react-json-form/.devcontainer/spfx-startup.sh b/samples/react-json-form/.devcontainer/spfx-startup.sh new file mode 100644 index 000000000..456d6aea8 --- /dev/null +++ b/samples/react-json-form/.devcontainer/spfx-startup.sh @@ -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**********" \ No newline at end of file diff --git a/samples/react-json-form/src/Hooks/UseObject.ts b/samples/react-json-form/src/Hooks/UseObject.ts index 44319f81f..992217f3d 100644 --- a/samples/react-json-form/src/Hooks/UseObject.ts +++ b/samples/react-json-form/src/Hooks/UseObject.ts @@ -1,5 +1,6 @@ import { useState } from 'react'; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default function useObject(InitialValue?: T) { const [value, setValue] = useState(InitialValue ?? {} as T); const updateValue: (Updates: Partial) => void = (Updates: Partial) => setValue((prev) => ({ ...prev, ...Updates })) diff --git a/samples/react-json-form/src/Models/SaveObject.ts b/samples/react-json-form/src/Models/SaveObject.ts index a6833ccf4..1c64e9d24 100644 --- a/samples/react-json-form/src/Models/SaveObject.ts +++ b/samples/react-json-form/src/Models/SaveObject.ts @@ -2,5 +2,6 @@ import { IForm } from "./Form"; export interface SaveObject { form: IForm; + // eslint-disable-next-line @typescript-eslint/no-explicit-any response: any; } \ No newline at end of file diff --git a/samples/react-json-form/src/Util/Util.ts b/samples/react-json-form/src/Util/Util.ts index a8ba9ea43..33d38c184 100644 --- a/samples/react-json-form/src/Util/Util.ts +++ b/samples/react-json-form/src/Util/Util.ts @@ -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)); } diff --git a/samples/react-json-form/src/webparts/jsonForm/JsonFormWebPart.ts b/samples/react-json-form/src/webparts/jsonForm/JsonFormWebPart.ts index 53e147e56..2fe33448a 100644 --- a/samples/react-json-form/src/webparts/jsonForm/JsonFormWebPart.ts +++ b/samples/react-json-form/src/webparts/jsonForm/JsonFormWebPart.ts @@ -41,6 +41,7 @@ export default class JsonFormWebPart extends BaseClientSideWebPart this.properties.formJson = JSON.stringify({ ...JSON.parse(this.properties.formJson), ...updated }, null, 2), Mode: this.displayMode, ListId: this.properties.listId, diff --git a/samples/react-json-form/src/webparts/jsonForm/components/Fields/Field.tsx b/samples/react-json-form/src/webparts/jsonForm/components/Fields/Field.tsx index 05fba2b4b..fd3fca760 100644 --- a/samples/react-json-form/src/webparts/jsonForm/components/Fields/Field.tsx +++ b/samples/react-json-form/src/webparts/jsonForm/components/Fields/Field.tsx @@ -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 = (props: React.PropsWi />; case FieldType.Choice: return ({ key: x, text: x }))} selectedKey={form[field.Id] ?? ""} label={field.DisplayName} @@ -57,32 +59,34 @@ export const Field: React.FunctionComponent = (props: React.PropsWi /> case FieldType.MultiChoice: return ({ 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
- {(field.DisplayName != null || field.DisplayName != "") && } + {(field.DisplayName !== null || field.DisplayName !== "") && }
- {(field as IGroupField).Fields.map(f => )} + {(field as IGroupField).Fields.map((f, index) => )}
; 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 } diff --git a/samples/react-json-form/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx b/samples/react-json-form/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx index 3176cd3a0..ed9abf8b2 100644 --- a/samples/react-json-form/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx +++ b/samples/react-json-form/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx @@ -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 const t = val.key as FieldType; const updates: Partial = { 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).Options = []; - if (t == FieldType.Conditional) - if ((field as IConditionalField).Field == null) + if (t === FieldType.Conditional) + if ((field as IConditionalField).Field === null) (updates as Partial).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).Fields = []; (updates as Partial).Direction = GroupDirection.Horizontal; } @@ -60,12 +61,12 @@ export const FieldEditorDialog: React.FunctionComponent }} /> - {FieldType.Conditional != field.Type && updateValue({ DisplayName: val })} />} + {FieldType.Conditional !== field.Type && updateValue({ DisplayName: val })} />} - {[FieldType.Choice, FieldType.MultiChoice].some(x => x == field.Type) && } - {FieldType.Conditional == field.Type && } - {FieldType.FieldGroup == field.Type && } + {[FieldType.Choice, FieldType.MultiChoice].some(x => x === field.Type) && } + {FieldType.Conditional === field.Type && } + {FieldType.FieldGroup === field.Type && } props.delete()} styles={{ root: { backgroundColor: "#FF0000" }, rootHovered: { backgroundColor: "#D10000" }, rootChecked: { backgroundColor: "#A30000" } }} /> @@ -90,9 +91,12 @@ export const GroupFieldOptions: React.FunctionComponent 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 = (p return (<> {(field as IChoiceField).Options.map((val, index) => { - return
+ return
{ @@ -118,7 +122,7 @@ const ChoiceFieldOptions: React.FunctionComponent = (p options[index] = val updateValue({ Options: options }); }} /> - updateValue({ Options: (field as IChoiceField).Options.filter((_, i) => i != index) })} /> + updateValue({ Options: (field as IChoiceField).Options.filter((_, i) => i !== index) })} />
})} { @@ -136,7 +140,7 @@ interface IConditionalFieldOptionsProps { const ConditionalFieldOptions: React.FunctionComponent = (props: React.PropsWithChildren) => { 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 updateValue({ LookupFieldId: val.key as string })} /> - {targetField != null && + {targetField !== null && <> - {FieldType.Choice == targetField.Type && ({ key: x, text: x }))} selectedKey={(field as IConditionalField).MatchValue as string} onChange={(_, val) => updateValue({ MatchValue: val.text })} />} - {FieldType.Boolean == targetField.Type && updateValue({ MatchValue: val.key == true.toString() })} + onChange={(_, val) => updateValue({ MatchValue: val.key === true.toString() })} />} - {FieldType.Number == targetField.Type && } - {[FieldType.Text, FieldType.MultilineText].some(x => x == targetField.Type) && x === targetField.Type) && updateValue({ MatchValue: val })} diff --git a/samples/react-json-form/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx b/samples/react-json-form/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx index fc1533f3a..99b79f472 100644 --- a/samples/react-json-form/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx +++ b/samples/react-json-form/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx @@ -17,7 +17,7 @@ const MAX_NUMBER_OF_ITEMS_IN_GROUP: number = 5; export const FormFieldCustomizer: React.FunctionComponent = (props: React.PropsWithChildren) => { const { field } = props; - const [shouldEdit, setShouldEdit] = React.useState(false); + const [shouldEdit, setShouldEdit] = React.useState(false); const editDialog = /> - 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
{shouldEdit && editDialog}
- {(f.DisplayName != null || f.DisplayName != "") && } + {(f.DisplayName !== null || f.DisplayName !== "") && } setShouldEdit(true)} /> props.delete()} />
{f.Fields.map((child, index) => { @@ -55,9 +56,9 @@ export const FormFieldCustomizer: React.FunctionComponent 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
; } - 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}
- + setShouldEdit(true)} /> props.delete()} />
@@ -111,15 +112,15 @@ export const FormFieldCustomizer: React.FunctionComponent {shouldEdit && editDialog}
setShouldEdit(true)}> - {FieldType.Label == field.Type && } - {FieldType.Header == field.Type && {field.DisplayName}} - {FieldType.Text == field.Type && } - {FieldType.MultilineText == field.Type && } - {FieldType.Number == field.Type && } - {FieldType.Boolean == field.Type && } - {FieldType.Choice == field.Type && } - {FieldType.MultiChoice == field.Type && } - {FieldType.PlaceHolder == field.Type && Press here to setup the field!} + {FieldType.Label === field.Type && } + {FieldType.Header === field.Type && {field.DisplayName}} + {FieldType.Text === field.Type && } + {FieldType.MultilineText === field.Type && } + {FieldType.Number === field.Type && } + {FieldType.Boolean === field.Type && } + {FieldType.Choice === field.Type && } + {FieldType.MultiChoice === field.Type && } + {FieldType.PlaceHolder === field.Type && Press here to setup the field!}
diff --git a/samples/react-json-form/src/webparts/jsonForm/components/JsonForm.tsx b/samples/react-json-form/src/webparts/jsonForm/components/JsonForm.tsx index a8a63bb19..59d228355 100644 --- a/samples/react-json-form/src/webparts/jsonForm/components/JsonForm.tsx +++ b/samples/react-json-form/src/webparts/jsonForm/components/JsonForm.tsx @@ -21,42 +21,44 @@ export interface IJsonFormProps { export const JsonForm: React.FunctionComponent = (props: React.PropsWithChildren) => { const { Mode, ServerRelativeUrl } = props; const { value: Form, updateValue: UpdateForm, overwriteData: __SETFORM } = useObject(props.ServerRelativeUrl ? { Fields: [], Title: "" } : props.Form); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { value: filledForm, updateValue, overwriteData: __SETFILLEDFORM } = useObject(); const { provider } = React.useContext(SPFxContext); React.useEffect(() => { - if (ServerRelativeUrl != null) { - const fetch = async () => { + if (ServerRelativeUrl !== null) { + const fetch = async (): Promise => { 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 => { 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 return ( <> UpdateForm({ Title: val })} /> - {Mode == DisplayMode.Read && + {Mode === DisplayMode.Read && <> - {Form.Fields.map(field => { - return + {Form.Fields.map((field, index) => { + return })} - {props.ServerRelativeUrl == null && + {props.ServerRelativeUrl === null && saveForm()} /> @@ -64,19 +66,20 @@ export const JsonForm: React.FunctionComponent = (props: React.P } - {Mode == DisplayMode.Edit && + {Mode === DisplayMode.Edit && <> {Form.Fields.map((Field, index) => { return { - 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 }); }}