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:
commit
5a67f695b2
Binary file not shown.
After Width: | Height: | Size: 10 MiB |
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -4,6 +4,6 @@ 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 }))
|
||||
return {
|
||||
value, updateValue, overwriteData: setValue
|
||||
value, updateValue
|
||||
};
|
||||
}
|
|
@ -36,4 +36,9 @@ 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)[];
|
||||
}
|
|
@ -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 = () => {
|
||||
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_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[]) => {
|
||||
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));;
|
||||
}
|
||||
|
|
|
@ -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 '../../Models/Form';
|
||||
import { IDataProvider, SharePointProvider } from '../../Providers/SharePointProvider';
|
||||
import { IForm } from './model/FormField';
|
||||
import { SPFI, SPFx, spfi } from '@pnp/sp/presets/all'
|
||||
|
||||
export interface IJsonFormWebPartProps {
|
||||
formJson: string;
|
||||
|
@ -19,12 +19,12 @@ export interface IJsonFormWebPartProps {
|
|||
|
||||
export interface AppContext {
|
||||
context: BaseComponentContext;
|
||||
provider: IDataProvider;
|
||||
SP: SPFI;
|
||||
ListId: string;
|
||||
ItemId?: number
|
||||
}
|
||||
|
||||
export const SPFxContext = React.createContext<AppContext>(null);
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
export const FILLED_FORM_QUERY_KEY = "FormServerRelativeUrl";
|
||||
|
||||
export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebPartProps> {
|
||||
|
||||
|
@ -34,7 +34,9 @@ export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebP
|
|||
{
|
||||
value: {
|
||||
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
|
||||
},
|
||||
React.createElement<IJsonFormProps>(
|
||||
|
@ -42,9 +44,7 @@ export default class JsonFormWebPart extends BaseClientSideWebPart<IJsonFormWebP
|
|||
{
|
||||
Form: JSON.parse(this.properties.formJson),
|
||||
SaveForm: (updated: IForm) => this.properties.formJson = JSON.stringify({ ...JSON.parse(this.properties.formJson), ...updated }, null, 2),
|
||||
Mode: this.displayMode,
|
||||
ListId: this.properties.listId,
|
||||
ServerRelativeUrl: urlSearchParams.has(FILLED_FORM_QUERY_KEY) ? urlSearchParams.get(FILLED_FORM_QUERY_KEY) : null,
|
||||
Mode: this.displayMode
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Label } from 'office-ui-fabric-react';
|
||||
|
||||
|
@ -8,52 +8,49 @@ export interface IFieldProps {
|
|||
onChange: (updates: object) => void;
|
||||
form: any;
|
||||
field: IField;
|
||||
readonly: boolean;
|
||||
}
|
||||
|
||||
export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWithChildren<IFieldProps>) => {
|
||||
const { field, onChange, form, readonly } = props;
|
||||
const { field, onChange, form } = props;
|
||||
switch (field.Type) {
|
||||
case FieldType.Label: return <Label>{field.DisplayName}</Label>;
|
||||
|
||||
case FieldType.Header: return <Text variant='xLarge'>{field.DisplayName}</Text>;
|
||||
|
||||
case FieldType.Text: return <TextField
|
||||
value={form[field.Id] ?? ""}
|
||||
value={form[field.Id]}
|
||||
label={field.DisplayName}
|
||||
readOnly={readonly}
|
||||
onChange={(_, val) => onChange({ [field.Id]: val })}
|
||||
/>;
|
||||
|
||||
case FieldType.MultilineText: return <TextField
|
||||
value={form[field.Id] ?? ""}
|
||||
value={form[field.Id]}
|
||||
label={field.DisplayName}
|
||||
rows={5}
|
||||
multiline
|
||||
readOnly={readonly}
|
||||
onChange={(_, val) => onChange({ [field.Id]: val })}
|
||||
/>;
|
||||
|
||||
case FieldType.Number: return <SpinButton
|
||||
inputMode='numeric'
|
||||
labelPosition={Position.top}
|
||||
value={form[field.Id] ?? ""}
|
||||
value={form[field.Id]}
|
||||
label={field.DisplayName}
|
||||
onChange={readonly ? null : (_, val) => onChange({ [field.Id]: Number(val) })}
|
||||
onChange={(_, val) => onChange({ [field.Id]: Number(val) })}
|
||||
/>;
|
||||
|
||||
case FieldType.Boolean: return <Checkbox
|
||||
label={field.DisplayName}
|
||||
checked={form[field.Id] ?? false}
|
||||
onChange={readonly ? () => null : (_, val) => onChange({ [field.Id]: val })}
|
||||
checked={form[field.Id]}
|
||||
onChange={(_, val) => onChange({ [field.Id]: val })}
|
||||
styles={{ root: { alignItems: 'center', marginTop: "1.75em" } }}
|
||||
/>;
|
||||
|
||||
case FieldType.Choice: return <Dropdown
|
||||
options={(field as any as IChoiceField).Options.map(x => ({ key: x, text: x }))}
|
||||
selectedKey={form[field.Id] ?? ""}
|
||||
selectedKey={form[field.Id]}
|
||||
label={field.DisplayName}
|
||||
onChange={readonly ? null : (_, option) => onChange({ [field.Id]: option.key })}
|
||||
onChange={(_, option) => onChange({ [field.Id]: option.key })}
|
||||
/>
|
||||
|
||||
case FieldType.MultiChoice: return <Dropdown
|
||||
|
@ -61,7 +58,7 @@ export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWi
|
|||
selectedKeys={form[field.Id] ?? []}
|
||||
label={field.DisplayName}
|
||||
multiSelect
|
||||
onChange={readonly ? null : (_, val) => {
|
||||
onChange={(_, 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 })
|
||||
|
@ -84,7 +81,7 @@ export const Field: React.FunctionComponent<IFieldProps> = (props: React.PropsWi
|
|||
const f = (field as IConditionalField);
|
||||
const visible = form[f.LookupFieldId] == f.MatchValue
|
||||
if (!visible) return <></>
|
||||
return <Field readonly={readonly} field={f.Field} form={form} onChange={onChange} />
|
||||
return <Field field={f.Field} form={form} onChange={onChange} />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 useObject from '../../../../Hooks/UseObject';
|
||||
import { NewField } from '../../../../Util/Util';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||
import { FieldEditorDialog } from './FieldEditorDialog';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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';
|
||||
|
@ -7,42 +8,22 @@ 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 { FILLED_FORM_QUERY_KEY, SPFxContext } from '../JsonFormWebPart';
|
||||
import { IForm } from '../../../Models/Form';
|
||||
import { SPFxContext } from '../JsonFormWebPart';
|
||||
|
||||
export interface IJsonFormProps {
|
||||
Form: IForm;
|
||||
SaveForm: (updated: IForm) => void;
|
||||
Mode: DisplayMode;
|
||||
ServerRelativeUrl?: string;
|
||||
ListId: string;
|
||||
}
|
||||
|
||||
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);
|
||||
const { value: filledForm, updateValue, overwriteData: __SETFILLEDFORM } = useObject<any>();
|
||||
const { provider } = React.useContext(SPFxContext);
|
||||
const { Mode } = props;
|
||||
const { value: Form, updateValue: UpdateForm } = useObject<IForm>(props.Form);
|
||||
const { value: filledForm, updateValue } = useObject<any>();
|
||||
const { ListId } = React.useContext(SPFxContext);
|
||||
|
||||
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 == "")
|
||||
console.log(ListId);
|
||||
if (ListId == null || ListId == "")
|
||||
return <Placeholder description={'Open the property pane and select a list to store responses'} iconName={'Edit'} iconText={'Please configure web part'} />
|
||||
|
||||
return (
|
||||
|
@ -53,14 +34,12 @@ export const JsonForm: React.FunctionComponent<IJsonFormProps> = (props: React.P
|
|||
<>
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
{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>
|
||||
{props.ServerRelativeUrl == null &&
|
||||
<DialogFooter>
|
||||
<PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => saveForm()} />
|
||||
</DialogFooter>
|
||||
}
|
||||
<DialogFooter>
|
||||
<PrimaryButton text='Submit' iconProps={{ iconName: "Accept" }} onClick={() => alert(JSON.stringify(filledForm, null, 2))} />
|
||||
</DialogFooter>
|
||||
</>
|
||||
}
|
||||
|
||||
|
|
|
@ -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)[];
|
||||
}
|
Loading…
Reference in New Issue