Merge branch 'main' into temp

# Conflicts:
#	samples/react-json-form/README.md
#	samples/react-json-form/src/Models/Form.ts
#	samples/react-json-form/src/Models/FormField.ts
#	samples/react-json-form/src/Models/SaveObject.ts
#	samples/react-json-form/src/Providers/SharePointProvider.ts
#	samples/react-json-form/src/webparts/jsonForm/model/FormField.ts
#	samples/react-jsonForm/src/Models/FormField.ts
This commit is contained in:
Dan Toft 2023-05-16 20:48:20 +02:00
commit 5a67f695b2
12 changed files with 138 additions and 63 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-json-form",
"source": "pnp",
"title": "Json form builder",
"shortDescription": "Build a custom form with ease",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-json-form",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-json-form",
"longDescription": [
"Really easy way for the users to build and use a custom form without having to worry about having to setup a SharePoint list, instead stores the data directly in a document library"
],
"creationDateTime": "2023-05-16",
"updateDateTime": "2023-05-16",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.17.1"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-caml2table/assets/Demo.gif",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "Tanddant",
"pictureUrl": "https://github.com/Tanddant.png",
"name": "Dan Toft"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

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, overwriteData: setValue value, updateValue
}; };
} }

View File

@ -36,4 +36,9 @@ 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

@ -1,4 +1,4 @@
import { FieldType, IConditionalField, IField, IGroupField } from "../Models/FormField"; import { FieldType, IConditionalField, IField, IGroupField } from "../webparts/jsonForm/model/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_LOOKUP_FIELDTYPES: FieldType[] = [FieldType.PlaceHolder, FieldType.MultiChoice, FieldType.PlaceHolder, FieldType.Label]; const UNSUPPORTED_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_LOOKUP_FIELDTYPES.some(bannedType => bannedType == x.Type));; return arr.filter(x => x.DisplayName != null && !UNSUPPORTED_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 '../../Models/Form'; import { IForm } from './model/FormField';
import { IDataProvider, SharePointProvider } from '../../Providers/SharePointProvider'; import { SPFI, SPFx, spfi } from '@pnp/sp/presets/all'
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;
provider: IDataProvider; SP: SPFI;
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,7 +34,9 @@ export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebP
{ {
value: { value: {
context: this.context, context: this.context,
provider: new SharePointProvider(this.context, this.properties.listId) SP: spfi().using(SPFx(this.context)),
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>(
@ -42,9 +44,7 @@ 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 '../../../../Models/FormField'; import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../model/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,52 +8,49 @@ 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, readonly } = props; const { field, onChange, form } = 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={readonly ? null : (_, val) => onChange({ [field.Id]: Number(val) })} onChange={(_, 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] ?? false} checked={form[field.Id]}
onChange={readonly ? () => null : (_, val) => onChange({ [field.Id]: val })} onChange={(_, 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={readonly ? null : (_, option) => onChange({ [field.Id]: option.key })} onChange={(_, option) => onChange({ [field.Id]: option.key })}
/> />
case FieldType.MultiChoice: return <Dropdown case FieldType.MultiChoice: return <Dropdown
@ -61,7 +58,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={readonly ? null : (_, val) => { onChange={(_, 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 })
@ -84,7 +81,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 readonly={readonly} field={f.Field} form={form} onChange={onChange} /> return <Field 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 '../../../../Models/FormField'; import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../model/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 '../../../../Models/FormField'; import { FieldType, GroupDirection, IChoiceField, IConditionalField, IField, IGroupField } from '../../model/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,4 +1,5 @@
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';
@ -7,42 +8,22 @@ 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 { FILLED_FORM_QUERY_KEY, SPFxContext } from '../JsonFormWebPart'; import { 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, ServerRelativeUrl } = props; const { Mode } = props;
const { value: Form, updateValue: UpdateForm, overwriteData: __SETFORM } = useObject<IForm>(props.ServerRelativeUrl ? { Fields: [], Title: "" } : props.Form); const { value: Form, updateValue: UpdateForm } = useObject<IForm>(props.Form);
const { value: filledForm, updateValue, overwriteData: __SETFILLEDFORM } = useObject<any>(); const { value: filledForm, updateValue } = useObject<any>();
const { provider } = React.useContext(SPFxContext); const { ListId } = React.useContext(SPFxContext);
React.useEffect(() => { console.log(ListId);
if (ServerRelativeUrl != null) { if (ListId == null || ListId == "")
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 (
@ -53,14 +34,12 @@ 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 readonly={props.ServerRelativeUrl != null} field={field} onChange={updateValue} form={filledForm} /> return <Field field={field} onChange={updateValue} form={filledForm} />
})} })}
</Stack> </Stack>
{props.ServerRelativeUrl == null && <DialogFooter>
<DialogFooter> <PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => alert(JSON.stringify(filledForm, null, 2))} />
<PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => saveForm()} /> </DialogFooter>
</DialogFooter>
}
</> </>
} }

View File

@ -0,0 +1,44 @@
export enum FieldType {
Text = "Text",
MultilineText = "MultilineText",
Number = "Number",
Boolean = "Boolean",
Choice = "Choice",
MultiChoice = "MultiChoice",
FieldGroup = "FieldGroup",
PlaceHolder = "PlaceHolder",
Label = "Label",
Conditional = "Conditional",
Header = "Header"
}
export interface IField {
Id: string;
DisplayName?: string;
Type: FieldType;
}
export interface IChoiceField extends IField {
Options: string[]
}
export interface IConditionalField extends IField {
LookupFieldId: string;
MatchValue: string | number | boolean;
Field: IField;
}
export enum GroupDirection {
Vertical,
Horizontal
}
export interface IGroupField extends IField {
Fields: (IField | IChoiceField | IGroupField | IConditionalField)[]
Direction: GroupDirection;
}
export interface IForm {
Title: string;
Fields: (IField | IChoiceField | IGroupField | IConditionalField)[];
}