From af84afa787074731d710b6580ff8b5dc29bcd60b Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Thu, 11 May 2023 21:23:32 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20-=20[react-jsonForm]:=20Submit?= =?UTF-8?q?=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- samples/react-jsonForm/src/Hooks/UseObject.ts | 2 +- samples/react-jsonForm/src/Models/Form.ts | 6 +++ .../jsonForm/model => Models}/FormField.ts | 5 --- .../react-jsonForm/src/Models/SaveObject.ts | 6 +++ .../src/Providers/SharePointProvider.ts | 30 +++++++++++++ samples/react-jsonForm/src/Util/Util.ts | 6 +-- .../src/webparts/jsonForm/JsonFormWebPart.ts | 18 ++++---- .../jsonForm/components/Fields/Field.tsx | 27 ++++++----- .../components/Fields/FieldEditorDialog.tsx | 2 +- .../components/Fields/FormFieldCustomizer.tsx | 2 +- .../webparts/jsonForm/components/JsonForm.tsx | 45 ++++++++++++++----- 11 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 samples/react-jsonForm/src/Models/Form.ts rename samples/react-jsonForm/src/{webparts/jsonForm/model => Models}/FormField.ts (87%) create mode 100644 samples/react-jsonForm/src/Models/SaveObject.ts create mode 100644 samples/react-jsonForm/src/Providers/SharePointProvider.ts diff --git a/samples/react-jsonForm/src/Hooks/UseObject.ts b/samples/react-jsonForm/src/Hooks/UseObject.ts index 6784f9ad1..44319f81f 100644 --- a/samples/react-jsonForm/src/Hooks/UseObject.ts +++ b/samples/react-jsonForm/src/Hooks/UseObject.ts @@ -4,6 +4,6 @@ export default function useObject(InitialValue?: T) { const [value, setValue] = useState(InitialValue ?? {} as T); const updateValue: (Updates: Partial) => void = (Updates: Partial) => setValue((prev) => ({ ...prev, ...Updates })) return { - value, updateValue + value, updateValue, overwriteData: setValue }; } \ No newline at end of file diff --git a/samples/react-jsonForm/src/Models/Form.ts b/samples/react-jsonForm/src/Models/Form.ts new file mode 100644 index 000000000..45f19b04f --- /dev/null +++ b/samples/react-jsonForm/src/Models/Form.ts @@ -0,0 +1,6 @@ +import { IChoiceField, IConditionalField, IField, IGroupField } from "./FormField"; + +export interface IForm { + Title: string; + Fields: (IField | IChoiceField | IGroupField | IConditionalField)[]; +} \ No newline at end of file diff --git a/samples/react-jsonForm/src/webparts/jsonForm/model/FormField.ts b/samples/react-jsonForm/src/Models/FormField.ts similarity index 87% rename from samples/react-jsonForm/src/webparts/jsonForm/model/FormField.ts rename to samples/react-jsonForm/src/Models/FormField.ts index 64a75c999..4672841a8 100644 --- a/samples/react-jsonForm/src/webparts/jsonForm/model/FormField.ts +++ b/samples/react-jsonForm/src/Models/FormField.ts @@ -36,9 +36,4 @@ export enum GroupDirection { export interface IGroupField extends IField { Fields: (IField | IChoiceField | IGroupField | IConditionalField)[] Direction: GroupDirection; -} - -export interface IForm { - Title: string; - Fields: (IField | IChoiceField | IGroupField | IConditionalField)[]; } \ No newline at end of file diff --git a/samples/react-jsonForm/src/Models/SaveObject.ts b/samples/react-jsonForm/src/Models/SaveObject.ts new file mode 100644 index 000000000..a6833ccf4 --- /dev/null +++ b/samples/react-jsonForm/src/Models/SaveObject.ts @@ -0,0 +1,6 @@ +import { IForm } from "./Form"; + +export interface SaveObject { + form: IForm; + response: any; +} \ No newline at end of file diff --git a/samples/react-jsonForm/src/Providers/SharePointProvider.ts b/samples/react-jsonForm/src/Providers/SharePointProvider.ts new file mode 100644 index 000000000..2759e079d --- /dev/null +++ b/samples/react-jsonForm/src/Providers/SharePointProvider.ts @@ -0,0 +1,30 @@ +import { BaseComponentContext } from "@microsoft/sp-component-base" +import { SaveObject } from "../Models/SaveObject"; +import { SPFI, SPFx, spfi } from '@pnp/sp/presets/all' + +export interface IDataProvider { + SaveSubmission: (Submission: SaveObject) => Promise; + GetSubmission: (ServerRelativeUrl: string) => Promise; +} + +export class SharePointProvider implements IDataProvider { + private SP: SPFI; + private LIST_ID: string; + + constructor(context: BaseComponentContext, ListID: string) { + this.SP = spfi().using(SPFx(context)); + this.LIST_ID = ListID; + } + + public async SaveSubmission(Submission: SaveObject): Promise { + const item = await this.SP.web.lists.getById(this.LIST_ID).rootFolder.files.addUsingPath(`${new Date().getTime()}.json`, JSON.stringify(Submission, null, 2)) + return item.data.ServerRelativeUrl; + } + + public async GetSubmission(ServerRelativeUrl: string): Promise { + const form = await this.SP.web.getFileByServerRelativePath(ServerRelativeUrl).getText(); + return JSON.parse(form); + } + + +} \ No newline at end of file diff --git a/samples/react-jsonForm/src/Util/Util.ts b/samples/react-jsonForm/src/Util/Util.ts index 9ed6055f1..a8ba9ea43 100644 --- a/samples/react-jsonForm/src/Util/Util.ts +++ b/samples/react-jsonForm/src/Util/Util.ts @@ -1,4 +1,4 @@ -import { FieldType, IConditionalField, IField, IGroupField } from "../webparts/jsonForm/model/FormField"; +import { FieldType, IConditionalField, IField, IGroupField } from "../Models/FormField"; export const generateGuid: () => string = () => { return Math.random().toString(36).substring(2, 15) + @@ -8,7 +8,7 @@ export const generateGuid: () => string = () => { export const NewField: () => IField = () => ({ Id: generateGuid(), Type: FieldType.PlaceHolder } as IField) -const UNSUPPORTED_FIELDTYPES: FieldType[] = [FieldType.PlaceHolder, FieldType.MultiChoice, FieldType.PlaceHolder, FieldType.Label]; +const UNSUPPORTED_LOOKUP_FIELDTYPES: FieldType[] = [FieldType.PlaceHolder, FieldType.MultiChoice, FieldType.PlaceHolder, FieldType.Label]; export const GetLookupFields: (Fields: IField[]) => IField[] = (Fields: IField[]) => { const arr: IField[] = []; @@ -27,5 +27,5 @@ export const GetLookupFields: (Fields: IField[]) => IField[] = (Fields: IField[] } } - return arr.filter(x => x.DisplayName != null && !UNSUPPORTED_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-jsonForm/src/webparts/jsonForm/JsonFormWebPart.ts b/samples/react-jsonForm/src/webparts/jsonForm/JsonFormWebPart.ts index 75f5fa5fd..53e147e56 100644 --- a/samples/react-jsonForm/src/webparts/jsonForm/JsonFormWebPart.ts +++ b/samples/react-jsonForm/src/webparts/jsonForm/JsonFormWebPart.ts @@ -9,8 +9,8 @@ import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; import { PropertyFieldCodeEditor, PropertyFieldCodeEditorLanguages } from '@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor'; import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker'; import { IJsonFormProps, JsonForm } from './components/JsonForm'; -import { IForm } from './model/FormField'; -import { SPFI, SPFx, spfi } from '@pnp/sp/presets/all' +import { IForm } from '../../Models/Form'; +import { IDataProvider, SharePointProvider } from '../../Providers/SharePointProvider'; export interface IJsonFormWebPartProps { formJson: string; @@ -19,12 +19,12 @@ export interface IJsonFormWebPartProps { export interface AppContext { context: BaseComponentContext; - SP: SPFI; - ListId: string; - ItemId?: number + provider: IDataProvider; } + export const SPFxContext = React.createContext(null); const urlSearchParams = new URLSearchParams(window.location.search); +export const FILLED_FORM_QUERY_KEY = "FormServerRelativeUrl"; export default class JsonFormWebPart extends BaseClientSideWebPart { @@ -34,9 +34,7 @@ export default class JsonFormWebPart extends BaseClientSideWebPart( @@ -44,7 +42,9 @@ export default class JsonFormWebPart extends BaseClientSideWebPart this.properties.formJson = JSON.stringify({ ...JSON.parse(this.properties.formJson), ...updated }, null, 2), - Mode: this.displayMode + Mode: this.displayMode, + ListId: this.properties.listId, + ServerRelativeUrl: urlSearchParams.has(FILLED_FORM_QUERY_KEY) ? urlSearchParams.get(FILLED_FORM_QUERY_KEY) : null, } ) ); diff --git a/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/Field.tsx b/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/Field.tsx index 63ed34f90..05fba2b4b 100644 --- a/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/Field.tsx +++ b/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/Field.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../model/FormField'; +import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../../../Models/FormField'; import { Position, SpinButton, TextField, Checkbox, Dropdown, MessageBar, MessageBarType, Text } from '@fluentui/react'; import { Label } from 'office-ui-fabric-react'; @@ -8,49 +8,52 @@ export interface IFieldProps { onChange: (updates: object) => void; form: any; field: IField; + readonly: boolean; } export const Field: React.FunctionComponent = (props: React.PropsWithChildren) => { - const { field, onChange, form } = props; + const { field, onChange, form, readonly } = props; switch (field.Type) { case FieldType.Label: return ; case FieldType.Header: return {field.DisplayName}; case FieldType.Text: return onChange({ [field.Id]: val })} />; case FieldType.MultilineText: return onChange({ [field.Id]: val })} />; case FieldType.Number: return onChange({ [field.Id]: Number(val) })} + onChange={readonly ? null : (_, val) => onChange({ [field.Id]: Number(val) })} />; case FieldType.Boolean: return onChange({ [field.Id]: val })} + checked={form[field.Id] ?? false} + onChange={readonly ? () => null : (_, val) => onChange({ [field.Id]: val })} styles={{ root: { alignItems: 'center', marginTop: "1.75em" } }} />; case FieldType.Choice: return ({ key: x, text: x }))} - selectedKey={form[field.Id]} + selectedKey={form[field.Id] ?? ""} label={field.DisplayName} - onChange={(_, option) => onChange({ [field.Id]: option.key })} + onChange={readonly ? null : (_, option) => onChange({ [field.Id]: option.key })} /> case FieldType.MultiChoice: return = (props: React.PropsWi selectedKeys={form[field.Id] ?? []} label={field.DisplayName} multiSelect - onChange={(_, val) => { + onChange={readonly ? null : (_, val) => { let selected: string[] = form[field.Id] ?? []; selected = val.selected ? [...selected, val.key as string] : selected.filter(x => x != val.key); onChange({ [field.Id]: selected }) @@ -81,7 +84,7 @@ export const Field: React.FunctionComponent = (props: React.PropsWi const f = (field as IConditionalField); const visible = form[f.LookupFieldId] == f.MatchValue if (!visible) return <> - return + return } } diff --git a/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx b/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx index 2a399ebe7..3176cd3a0 100644 --- a/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx +++ b/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FieldEditorDialog.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../model/FormField'; +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'; import useObject from '../../../../Hooks/UseObject'; import { NewField } from '../../../../Util/Util'; diff --git a/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx b/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx index 2355d7738..fc1533f3a 100644 --- a/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx +++ b/samples/react-jsonForm/src/webparts/jsonForm/components/Fields/FormFieldCustomizer.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../model/FormField'; +import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../../../Models/FormField'; import { ActionButton, Checkbox, Dropdown, Label, MessageBar, MessageBarType, Position, PrimaryButton, SpinButton, Text, TextField, getTheme } from '@fluentui/react'; import { cloneDeep } from '@microsoft/sp-lodash-subset'; import { FieldEditorDialog } from './FieldEditorDialog'; diff --git a/samples/react-jsonForm/src/webparts/jsonForm/components/JsonForm.tsx b/samples/react-jsonForm/src/webparts/jsonForm/components/JsonForm.tsx index 2b973b901..a8a63bb19 100644 --- a/samples/react-jsonForm/src/webparts/jsonForm/components/JsonForm.tsx +++ b/samples/react-jsonForm/src/webparts/jsonForm/components/JsonForm.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { IForm } from '../model/FormField'; import useObject from '../../../Hooks/UseObject'; import { PrimaryButton, Stack, DialogFooter } from '@fluentui/react' import { Field } from './Fields/Field'; @@ -8,22 +7,42 @@ import { Placeholder, WebPartTitle } from '@pnp/spfx-controls-react'; import { FormFieldCustomizer } from './Fields/FormFieldCustomizer'; import { GetLookupFields, NewField } from '../../../Util/Util'; import { cloneDeep } from '@microsoft/sp-lodash-subset'; -import { SPFxContext } from '../JsonFormWebPart'; +import { FILLED_FORM_QUERY_KEY, SPFxContext } from '../JsonFormWebPart'; +import { IForm } from '../../../Models/Form'; export interface IJsonFormProps { Form: IForm; SaveForm: (updated: IForm) => void; Mode: DisplayMode; + ServerRelativeUrl?: string; + ListId: string; } export const JsonForm: React.FunctionComponent = (props: React.PropsWithChildren) => { - const { Mode } = props; - const { value: Form, updateValue: UpdateForm } = useObject(props.Form); - const { value: filledForm, updateValue } = useObject(); - const { ListId } = React.useContext(SPFxContext); + const { Mode, ServerRelativeUrl } = props; + const { value: Form, updateValue: UpdateForm, overwriteData: __SETFORM } = useObject(props.ServerRelativeUrl ? { Fields: [], Title: "" } : props.Form); + const { value: filledForm, updateValue, overwriteData: __SETFILLEDFORM } = useObject(); + const { provider } = React.useContext(SPFxContext); - console.log(ListId); - if (ListId == null || ListId == "") + React.useEffect(() => { + if (ServerRelativeUrl != null) { + const fetch = async () => { + const result = await provider.GetSubmission(ServerRelativeUrl); + __SETFILLEDFORM(result.response); + __SETFORM(result.form); + } + fetch(); + } + }, []) + + const saveForm = async () => { + const serverRelativeUrl = await provider.SaveSubmission({ form: Form, response: filledForm }); + var searchParams = new URLSearchParams(window.location.search); + searchParams.set(FILLED_FORM_QUERY_KEY, serverRelativeUrl); + window.location.search = searchParams.toString(); + } + + if (props.ListId == null || props.ListId == "") return return ( @@ -34,12 +53,14 @@ export const JsonForm: React.FunctionComponent = (props: React.P <> {Form.Fields.map(field => { - return + return })} - - alert(JSON.stringify(filledForm, null, 2))} /> - + {props.ServerRelativeUrl == null && + + saveForm()} /> + + } }