🎉 - [react-jsonForm]: Submit form

This commit is contained in:
Dan Toft 2023-05-11 21:23:32 +02:00
parent dd8d73f31f
commit af84afa787
11 changed files with 105 additions and 44 deletions

View File

@ -4,6 +4,6 @@ export default function useObject<T>(InitialValue?: T) {
const [value, setValue] = useState<T>(InitialValue ?? {} as T); const [value, setValue] = useState<T>(InitialValue ?? {} as T);
const updateValue: (Updates: Partial<T>) => void = (Updates: Partial<T>) => setValue((prev) => ({ ...prev, ...Updates })) const updateValue: (Updates: Partial<T>) => void = (Updates: Partial<T>) => setValue((prev) => ({ ...prev, ...Updates }))
return { return {
value, updateValue value, updateValue, overwriteData: setValue
}; };
} }

View File

@ -0,0 +1,6 @@
import { IChoiceField, IConditionalField, IField, IGroupField } from "./FormField";
export interface IForm {
Title: string;
Fields: (IField | IChoiceField | IGroupField | IConditionalField)[];
}

View File

@ -36,9 +36,4 @@ export enum GroupDirection {
export interface IGroupField extends IField { export interface IGroupField extends IField {
Fields: (IField | IChoiceField | IGroupField | IConditionalField)[] Fields: (IField | IChoiceField | IGroupField | IConditionalField)[]
Direction: GroupDirection; Direction: GroupDirection;
}
export interface IForm {
Title: string;
Fields: (IField | IChoiceField | IGroupField | IConditionalField)[];
} }

View File

@ -0,0 +1,6 @@
import { IForm } from "./Form";
export interface SaveObject {
form: IForm;
response: any;
}

View File

@ -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<string>;
GetSubmission: (ServerRelativeUrl: string) => Promise<SaveObject>;
}
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<string> {
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<SaveObject> {
const form = await this.SP.web.getFileByServerRelativePath(ServerRelativeUrl).getText();
return JSON.parse(form);
}
}

View File

@ -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 = () => { export const generateGuid: () => string = () => {
return Math.random().toString(36).substring(2, 15) + 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) 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[]) => { export const GetLookupFields: (Fields: IField[]) => IField[] = (Fields: IField[]) => {
const arr: 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));;
} }

View File

@ -9,8 +9,8 @@ import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { PropertyFieldCodeEditor, PropertyFieldCodeEditorLanguages } from '@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor'; import { PropertyFieldCodeEditor, PropertyFieldCodeEditorLanguages } from '@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor';
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker'; import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
import { IJsonFormProps, JsonForm } from './components/JsonForm'; import { IJsonFormProps, JsonForm } from './components/JsonForm';
import { IForm } from './model/FormField'; import { IForm } from '../../Models/Form';
import { SPFI, SPFx, spfi } from '@pnp/sp/presets/all' import { IDataProvider, SharePointProvider } from '../../Providers/SharePointProvider';
export interface IJsonFormWebPartProps { export interface IJsonFormWebPartProps {
formJson: string; formJson: string;
@ -19,12 +19,12 @@ export interface IJsonFormWebPartProps {
export interface AppContext { export interface AppContext {
context: BaseComponentContext; context: BaseComponentContext;
SP: SPFI; provider: IDataProvider;
ListId: string;
ItemId?: number
} }
export const SPFxContext = React.createContext<AppContext>(null); export const SPFxContext = React.createContext<AppContext>(null);
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
export const FILLED_FORM_QUERY_KEY = "FormServerRelativeUrl";
export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebPartProps> { export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebPartProps> {
@ -34,9 +34,7 @@ export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebP
{ {
value: { value: {
context: this.context, context: this.context,
SP: spfi().using(SPFx(this.context)), provider: new SharePointProvider(this.context, this.properties.listId)
ListId: urlSearchParams.get("ListId") ?? this.properties.listId,
ItemId: urlSearchParams.has("ItemId") ? parseInt(urlSearchParams.get("ItemId")) : null
} as AppContext } as AppContext
}, },
React.createElement<IJsonFormProps>( React.createElement<IJsonFormProps>(
@ -44,7 +42,9 @@ export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebP
{ {
Form: JSON.parse(this.properties.formJson), Form: JSON.parse(this.properties.formJson),
SaveForm: (updated: IForm) => this.properties.formJson = JSON.stringify({ ...JSON.parse(this.properties.formJson), ...updated }, null, 2), SaveForm: (updated: IForm) => 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,
} }
) )
); );

View File

@ -1,5 +1,5 @@
import * as React from 'react'; 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 { Position, SpinButton, TextField, Checkbox, Dropdown, MessageBar, MessageBarType, Text } from '@fluentui/react';
import { Label } from 'office-ui-fabric-react'; import { Label } from 'office-ui-fabric-react';
@ -8,49 +8,52 @@ export interface IFieldProps {
onChange: (updates: object) => void; onChange: (updates: object) => void;
form: any; form: any;
field: IField; field: IField;
readonly: boolean;
} }
export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWithChildren<IFieldProps>) => { export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWithChildren<IFieldProps>) => {
const { field, onChange, form } = props; const { field, onChange, form, readonly } = props;
switch (field.Type) { switch (field.Type) {
case FieldType.Label: return <Label>{field.DisplayName}</Label>; case FieldType.Label: return <Label>{field.DisplayName}</Label>;
case FieldType.Header: return <Text variant='xLarge'>{field.DisplayName}</Text>; case FieldType.Header: return <Text variant='xLarge'>{field.DisplayName}</Text>;
case FieldType.Text: return <TextField case FieldType.Text: return <TextField
value={form[field.Id]} value={form[field.Id] ?? ""}
label={field.DisplayName} label={field.DisplayName}
readOnly={readonly}
onChange={(_, val) => onChange({ [field.Id]: val })} onChange={(_, val) => onChange({ [field.Id]: val })}
/>; />;
case FieldType.MultilineText: return <TextField case FieldType.MultilineText: return <TextField
value={form[field.Id]} value={form[field.Id] ?? ""}
label={field.DisplayName} label={field.DisplayName}
rows={5} rows={5}
multiline multiline
readOnly={readonly}
onChange={(_, val) => onChange({ [field.Id]: val })} onChange={(_, val) => onChange({ [field.Id]: val })}
/>; />;
case FieldType.Number: return <SpinButton case FieldType.Number: return <SpinButton
inputMode='numeric' inputMode='numeric'
labelPosition={Position.top} labelPosition={Position.top}
value={form[field.Id]} value={form[field.Id] ?? ""}
label={field.DisplayName} label={field.DisplayName}
onChange={(_, val) => onChange({ [field.Id]: Number(val) })} onChange={readonly ? null : (_, val) => onChange({ [field.Id]: Number(val) })}
/>; />;
case FieldType.Boolean: return <Checkbox case FieldType.Boolean: return <Checkbox
label={field.DisplayName} label={field.DisplayName}
checked={form[field.Id]} checked={form[field.Id] ?? false}
onChange={(_, val) => onChange({ [field.Id]: val })} onChange={readonly ? () => null : (_, val) => onChange({ [field.Id]: val })}
styles={{ root: { alignItems: 'center', marginTop: "1.75em" } }} styles={{ root: { alignItems: 'center', marginTop: "1.75em" } }}
/>; />;
case FieldType.Choice: return <Dropdown case FieldType.Choice: return <Dropdown
options={(field as any as IChoiceField).Options.map(x => ({ key: x, text: x }))} options={(field as any as IChoiceField).Options.map(x => ({ key: x, text: x }))}
selectedKey={form[field.Id]} selectedKey={form[field.Id] ?? ""}
label={field.DisplayName} label={field.DisplayName}
onChange={(_, option) => onChange({ [field.Id]: option.key })} onChange={readonly ? null : (_, option) => onChange({ [field.Id]: option.key })}
/> />
case FieldType.MultiChoice: return <Dropdown case FieldType.MultiChoice: return <Dropdown
@ -58,7 +61,7 @@ export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWi
selectedKeys={form[field.Id] ?? []} selectedKeys={form[field.Id] ?? []}
label={field.DisplayName} label={field.DisplayName}
multiSelect multiSelect
onChange={(_, val) => { onChange={readonly ? null : (_, val) => {
let selected: string[] = form[field.Id] ?? []; 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 }) onChange({ [field.Id]: selected })
@ -81,7 +84,7 @@ export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWi
const f = (field as IConditionalField); const f = (field as IConditionalField);
const visible = form[f.LookupFieldId] == f.MatchValue const visible = form[f.LookupFieldId] == f.MatchValue
if (!visible) return <></> if (!visible) return <></>
return <Field field={f.Field} form={form} onChange={onChange} /> return <Field readonly={readonly} field={f.Field} form={form} onChange={onChange} />
} }
} }

View File

@ -1,5 +1,5 @@
import * as React from 'react'; 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 { ActionButton, ChoiceGroup, DefaultButton, Dialog, DialogFooter, Dropdown, Label, Position, PrimaryButton, SpinButton, Stack, TextField } from '@fluentui/react';
import useObject from '../../../../Hooks/UseObject'; import useObject from '../../../../Hooks/UseObject';
import { NewField } from '../../../../Util/Util'; import { NewField } from '../../../../Util/Util';

View File

@ -1,5 +1,5 @@
import * as React from 'react'; 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 { ActionButton, Checkbox, Dropdown, Label, MessageBar, MessageBarType, Position, PrimaryButton, SpinButton, Text, TextField, getTheme } from '@fluentui/react';
import { cloneDeep } from '@microsoft/sp-lodash-subset'; import { cloneDeep } from '@microsoft/sp-lodash-subset';
import { FieldEditorDialog } from './FieldEditorDialog'; import { FieldEditorDialog } from './FieldEditorDialog';

View File

@ -1,5 +1,4 @@
import * as React from 'react'; import * as React from 'react';
import { IForm } from '../model/FormField';
import useObject from '../../../Hooks/UseObject'; import useObject from '../../../Hooks/UseObject';
import { PrimaryButton, Stack, DialogFooter } from '@fluentui/react' import { PrimaryButton, Stack, DialogFooter } from '@fluentui/react'
import { Field } from './Fields/Field'; import { Field } from './Fields/Field';
@ -8,22 +7,42 @@ import { Placeholder, WebPartTitle } from '@pnp/spfx-controls-react';
import { FormFieldCustomizer } from './Fields/FormFieldCustomizer'; import { FormFieldCustomizer } from './Fields/FormFieldCustomizer';
import { GetLookupFields, NewField } from '../../../Util/Util'; import { GetLookupFields, NewField } from '../../../Util/Util';
import { cloneDeep } from '@microsoft/sp-lodash-subset'; 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 { export interface IJsonFormProps {
Form: IForm; Form: IForm;
SaveForm: (updated: IForm) => void; SaveForm: (updated: IForm) => void;
Mode: DisplayMode; Mode: DisplayMode;
ServerRelativeUrl?: string;
ListId: string;
} }
export const JsonForm: React.FunctionComponent<IJsonFormProps> = (props: React.PropsWithChildren<IJsonFormProps>) => { export const JsonForm: React.FunctionComponent<IJsonFormProps> = (props: React.PropsWithChildren<IJsonFormProps>) => {
const { Mode } = props; const { Mode, ServerRelativeUrl } = props;
const { value: Form, updateValue: UpdateForm } = useObject<IForm>(props.Form); const { value: Form, updateValue: UpdateForm, overwriteData: __SETFORM } = useObject<IForm>(props.ServerRelativeUrl ? { Fields: [], Title: "" } : props.Form);
const { value: filledForm, updateValue } = useObject<any>(); const { value: filledForm, updateValue, overwriteData: __SETFILLEDFORM } = useObject<any>();
const { ListId } = React.useContext(SPFxContext); const { provider } = React.useContext(SPFxContext);
console.log(ListId); React.useEffect(() => {
if (ListId == null || ListId == "") 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 <Placeholder description={'Open the property pane and select a list to store responses'} iconName={'Edit'} iconText={'Please configure web part'} /> return <Placeholder description={'Open the property pane and select a list to store responses'} iconName={'Edit'} iconText={'Please configure web part'} />
return ( return (
@ -34,12 +53,14 @@ export const JsonForm: React.FunctionComponent<IJsonFormProps> = (props: React.P
<> <>
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }}>
{Form.Fields.map(field => { {Form.Fields.map(field => {
return <Field field={field} onChange={updateValue} form={filledForm} /> return <Field readonly={props.ServerRelativeUrl != null} field={field} onChange={updateValue} form={filledForm} />
})} })}
</Stack> </Stack>
<DialogFooter> {props.ServerRelativeUrl == null &&
<PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => alert(JSON.stringify(filledForm, null, 2))} /> <DialogFooter>
</DialogFooter> <PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => saveForm()} />
</DialogFooter>
}
</> </>
} }