Skip to content

Added file handling in DynamicForm #1625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/documentation/docs/controls/DynamicForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ import { DynamicForm } from "@pnp/spfx-controls-react/lib/DynamicForm";
```
![DynamicForm](../assets/DynamicForm.png)

## File selection

To upload a file when creating a new document in a document library you need to specify:
- enableFileSelection: Set this parameter to true to enable file selection.
- contentTypeId: This parameter specifies the target content type ID of the document you are creating.
- supportedFileExtensions: This parameter is optional and is used to specify the supported file extensions if they are different from the default ones.

Enabling the file selection will display a new button on top of the form that allow the user to select a file from the recent files, browsing OneDrive or select and upload a file from the computer.

![DynamicFormWithFileSelection](../assets/DynamicFormWithFileSelection.png)

## Implementation

The `DynamicForm` can be configured with the following properties:
Expand All @@ -38,13 +49,15 @@ The `DynamicForm` can be configured with the following properties:
| contentTypeId | string | no | content type ID |
| disabled | boolean | no | Allows form to be disabled. Default value is `false`|
| disabledFields | string[] | no | InternalName of fields that should be disabled. Default value is `false`|
| enableFileSelection | boolean | no | Specify if the form should support the creation of a new list item in a document library attaching a file to it. This option is only available for document libraries and works only when the contentTypeId is specified and has a base type of type Document. Default value is `false`|
| hiddenFields | string[] | no | InternalName of fields that should be hidden. Default value is `false`|
| onListItemLoaded | (listItemData: any) => Promise<void> | no | List item loaded handler. Allows to access list item information after it's loaded.|
| onBeforeSubmit | (listItemData: any) => Promise<boolean> | no | Before submit handler. Allows to modify the object to be submitted or cancel the submission. To cancel, return `true`.|
| onSubmitted | (listItemData: any, listItem?: IItem) => void | no | Method that returns listItem data JSON object and PnPJS list item instance (`IItem`). |
| onSubmitError | (listItemData: any, error: Error) => void | no | Handler of submission error. |
| onCancelled | () => void | no | Handler when form has been cancelled. |
| returnListItemInstanceOnSubmit | boolean | no | Specifies if `onSubmitted` event should pass PnPJS list item (`IItem`) as a second parameter. Default - `true` |
| supportedFileExtensions | string[] | no | Specify the supported file extensions for the file picker. Only used when enableFileSelection is `true`. Default value is `["docx", "doc", "pptx", "ppt", "xlsx", "xls", "pdf"]`. |
| webAbsoluteUrl | string | no | Absolute Web Url of target site (user requires permissions). |
| fieldOverrides | {[columnInternalName: string] : {(fieldProperties: IDynamicFieldProps): React.ReactElement\<IDynamicFieldProps\>}} | no | Key value pair for fields you want to override. Key is the internal field name, value is the function to be called for the custom element to render. |
| respectEtag | boolean | no | Specifies if the form should respect the ETag of the item. Default - `true` |
Expand Down
5 changes: 5 additions & 0 deletions src/controls/dynamicForm/DynamicForm.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,8 @@
}
}
}

.selectedFileContainer {
display: flex;
margin: 10px 0px;
}
207 changes: 182 additions & 25 deletions src/controls/dynamicForm/DynamicForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IStackTokens, Stack } from "@fluentui/react/lib/Stack";
import * as React from "react";
import { IUploadImageResult } from "../../common/SPEntities";
import SPservice from "../../services/SPService";
import { IFilePickerResult } from "../filePicker";
import { FilePicker, IFilePickerResult } from "../filePicker";
import { DynamicField } from "./dynamicField";
import {
DateFormat,
Expand All @@ -27,6 +27,7 @@ import {
DialogFooter,
DialogType,
} from "@fluentui/react/lib/Dialog";
import { Icon } from 'office-ui-fabric-react';

import "@pnp/sp/lists";
import "@pnp/sp/content-types";
Expand Down Expand Up @@ -109,6 +110,11 @@ export class DynamicForm extends React.Component<
</div>
) : (
<div>
{this.props.enableFileSelection === true &&
this.props.listItemId === undefined &&
this.props.contentTypeId !== undefined &&
this.props.contentTypeId.startsWith("0x0101") &&
this.renderFileSelectionControl()}
{fieldCollection.map((v, i) => {
if (
fieldOverrides &&
Expand Down Expand Up @@ -182,6 +188,9 @@ export class DynamicForm extends React.Component<
onSubmitted,
onBeforeSubmit,
onSubmitError,
enableFileSelection,
validationErrorDialogProps,
returnListItemInstanceOnSubmit
} = this.props;

try {
Expand Down Expand Up @@ -218,11 +227,22 @@ export class DynamicForm extends React.Component<
}
}
});

if (shouldBeReturnBack) {
this.setState({
fieldCollection: fields,
isValidationErrorDialogOpen:
this.props.validationErrorDialogProps
validationErrorDialogProps
?.showDialogOnValidationError === true,
});
return;
}

if (enableFileSelection === true && this.state.selectedFile === undefined && this.props.listItemId === undefined) {
this.setState({
missingSelectedFile: true,
isValidationErrorDialogOpen:
validationErrorDialogProps
?.showDialogOnValidationError === true,
});
return;
Expand Down Expand Up @@ -322,7 +342,7 @@ export class DynamicForm extends React.Component<
if (onSubmitted) {
onSubmitted(
iur.data,
this.props.returnListItemInstanceOnSubmit !== false
returnListItemInstanceOnSubmit !== false
? iur.item
: undefined
);
Expand All @@ -338,28 +358,33 @@ export class DynamicForm extends React.Component<
else if (
contentTypeId === undefined ||
contentTypeId === "" ||
!contentTypeId.startsWith("0x0120")||
contentTypeId.startsWith("0x01")
(!contentTypeId.startsWith("0x0120") &&
contentTypeId.startsWith("0x01"))
) {
// We are adding a new list item
try {
const contentTypeIdField = "ContentTypeId";
//check if item contenttype is passed, then update the object with content type id, else, pass the object
if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId;
const iar = await sp.web.lists.getById(listId).items.add(objects);
if (onSubmitted) {
onSubmitted(
iar.data,
this.props.returnListItemInstanceOnSubmit !== false
? iar.item
: undefined
);
}
} catch (error) {
if (onSubmitError) {
onSubmitError(objects, error);
if (contentTypeId === undefined || enableFileSelection === true) {
await this.addFileToLibrary(objects);
}
else {
// We are adding a new list item
try {
const contentTypeIdField = "ContentTypeId";
//check if item contenttype is passed, then update the object with content type id, else, pass the object
if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId;
const iar = await sp.web.lists.getById(listId).items.add(objects);
if (onSubmitted) {
onSubmitted(
iar.data,
this.props.returnListItemInstanceOnSubmit !== false
? iar.item
: undefined
);
}
} catch (error) {
if (onSubmitError) {
onSubmitError(objects, error);
}
console.log("Error", error);
}
console.log("Error", error);
}
}
else if (contentTypeId.startsWith("0x0120")) {
Expand Down Expand Up @@ -408,6 +433,9 @@ export class DynamicForm extends React.Component<
}
console.log("Error", error);
}
} else if (contentTypeId.startsWith("0x01") && enableFileSelection === true) {
// We are adding a folder or a Document Set
await this.addFileToLibrary(objects);
}

this.setState({
Expand All @@ -422,6 +450,64 @@ export class DynamicForm extends React.Component<
}
};

private addFileToLibrary = async (objects: {}): Promise<void> => {
const {
selectedFile
} = this.state;

const {
listId,
contentTypeId,
onSubmitted,
onSubmitError,
returnListItemInstanceOnSubmit
} = this.props;

try {
const idField = "ID";
const contentTypeIdField = "ContentTypeId";

const library = await sp.web.lists.getById(listId);
const itemTitle =
selectedFile !== undefined && selectedFile.fileName !== undefined && selectedFile.fileName !== ""
? (selectedFile.fileName as string).replace(
/["|*|:|<|>|?|/|\\||]/g,
"_"
) // Replace not allowed chars in folder name
: ""; // Empty string will be replaced by SPO with Folder Item ID

const fileCreatedResult = await library.rootFolder.files.addChunked(encodeURI(itemTitle), await selectedFile.downloadFileContent());
const fields = await fileCreatedResult.file.listItemAllFields();

if (fields[idField]) {
// Read the ID of the just created folder or Document Set
const folderId = fields[idField];

// Set the content type ID for the target item
objects[contentTypeIdField] = contentTypeId;
// Update the just created folder or Document Set
const iur = await library.items.getById(folderId).update(objects);
if (onSubmitted) {
onSubmitted(
iur.data,
returnListItemInstanceOnSubmit !== false
? iur.item
: undefined
);
}
} else {
throw new Error(
"Unable to read the ID of the just created folder or Document Set"
);
}
} catch (error) {
if (onSubmitError) {
onSubmitError(objects, error);
}
console.log("Error", error);
}
}

// trigger when the user change any value in the form
private onChange = async (
internalName: string,
Expand Down Expand Up @@ -597,8 +683,8 @@ export class DynamicForm extends React.Component<
hiddenName = response.value;
termSetId = field.TermSetId;
anchorId = field.AnchorId;
if (item !== null) {
item[field.InternalName].forEach((element) => {
if (item !== null && item[field.InternalName] !== null && item[field.InternalName].results !== null) {
item[field.InternalName].results.forEach((element) => {
selectedTags.push({
key: element.TermGuid,
name: element.Label,
Expand Down Expand Up @@ -871,6 +957,77 @@ export class DynamicForm extends React.Component<
return errorMessage;
};

private renderFileSelectionControl = (): React.ReactElement => {
const {
selectedFile,
missingSelectedFile
} = this.state;

const labelEl = <label className={styles.fieldRequired + ' ' + styles.fieldLabel}>{strings.DynamicFormChooseFileLabel}</label>;

return <div>
<div className={styles.titleContainer}>
<Icon className={styles.fieldIcon} iconName={"DocumentSearch"} />
{labelEl}
</div>
<FilePicker
buttonLabel={strings.DynamicFormChooseFileButtonText}
accepts={this.props.supportedFileExtensions ? this.props.supportedFileExtensions : [".docx", ".doc", ".pptx", ".ppt", ".xlsx", ".xls", ".pdf"]}
onSave={(filePickerResult: IFilePickerResult[]) => {
if (filePickerResult.length === 1) {
this.setState({
selectedFile: filePickerResult[0],
missingSelectedFile: false
});
}
else {
this.setState({
missingSelectedFile: true
});
}
}}
required={true}
context={this.props.context}
hideWebSearchTab={true}
hideStockImages={true}
hideLocalMultipleUploadTab={true}
hideLinkUploadTab={true}
hideSiteFilesTab={true}
checkIfFileExists={true}
/>
{selectedFile && <div className={styles.selectedFileContainer}>
<Icon iconName={this.getFileIconFromExtension()} />
{selectedFile.fileName}
</div>}
{missingSelectedFile === true &&
<div className={styles.errormessage}>{strings.DynamicFormRequiredFileMessage}</div>}
</div>;
}

private getFileIconFromExtension = (): string => {
const fileExtension = this.state.selectedFile.fileName.split('.').pop();
switch (fileExtension) {
case 'pdf':
return 'PDF';
case 'docx':
case 'doc':
return 'WordDocument';
case 'pptx':
case 'ppt':
return 'PowerPointDocument';
case 'xlsx':
case 'xls':
return 'ExcelDocument';
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return 'FileImage';
default:
return 'Document';
}
}

private isEmptyNumOrString(value: string | number): boolean {
if ((value?.toString().trim().length || 0) === 0) return true;
}
Expand Down
13 changes: 13 additions & 0 deletions src/controls/dynamicForm/IDynamicFormProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,17 @@ export interface IDynamicFormProps {
* Specify validation error dialog properties
*/
validationErrorDialogProps?: IValidationErrorDialogProps;

/**
* Specify if the form should support the creation of a new list item in a document library attaching a file to it.
* This option is only available for document libraries and works only when the contentTypeId is specified and has a base type of type Document.
* Default - false
*/
enableFileSelection?: boolean;

/**
* Specify the supported file extensions for the file picker. Default - "docx", "doc", "pptx", "ppt", "xlsx", "xls", "pdf"
* Only used when enableFileSelection is true
*/
supportedFileExtensions?: string[];
}
8 changes: 4 additions & 4 deletions src/controls/dynamicForm/IDynamicFormState.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@

import { IInstalledLanguageInfo } from '@pnp/sp/regional-settings';
import { IDynamicFieldProps } from './dynamicField/IDynamicFieldProps';
import { IFilePickerResult } from "../filePicker";

export interface IDynamicFormState {
fieldCollection: IDynamicFieldProps[];
installedLanguages?: IInstalledLanguageInfo[];
isSaving?: boolean;
etag?: string;
isValidationErrorDialogOpen: boolean;
selectedFile?: IFilePickerResult;
missingSelectedFile?: boolean;
}



3 changes: 3 additions & 0 deletions src/loc/bg-bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ define([], () => {
"DynamicFormNumberValueMustBeGreaterThan": "Стойността трябва да е по-голяма от {0}",
"DynamicFormNumberValueMustBeBetween": "Стойността трябва да е между {0} и {1}",
"DynamicFormNumberValueMustBeLowerThan": "Стойността трябва да е по-ниска от {0}",
"DynamicFormChooseFileLabel": "File",
"DynamicFormChooseFileButtonText": "Select file",
"DynamicFormRequiredFileMessage": "File is required.",
"customDisplayName": "Използвайте това местоположение:",
"ListItemCommentDIalogDeleteSubText": "Наистина ли искате да изтриете този коментар?",
"ListItemCommentsDialogDeleteTitle": "Потвърдете Изтриване на коментар",
Expand Down
3 changes: 3 additions & 0 deletions src/loc/ca-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ define([], () => {
"DynamicFormNumberValueMustBeGreaterThan": "El valor ha de ser superior a {0}",
"DynamicFormNumberValueMustBeBetween": "El valor ha d'estar entre {0} i {1}",
"DynamicFormNumberValueMustBeLowerThan": "El valor ha de ser inferior a {0}",
"DynamicFormChooseFileLabel": "File",
"DynamicFormChooseFileButtonText": "Select file",
"DynamicFormRequiredFileMessage": "File is required.",
"customDisplayName": "Utilitzeu aquesta ubicació:",
"ListItemCommentDIalogDeleteSubText": "Esteu segur que voleu suprimir aquest comentari?",
"ListItemCommentsDialogDeleteTitle": "Confirmació de la supressió del comentari",
Expand Down
3 changes: 3 additions & 0 deletions src/loc/da-dk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ define([], () => {
"DynamicFormNumberValueMustBeGreaterThan": "Værdien skal være større end {0}",
"DynamicFormNumberValueMustBeBetween": "Værdien skal være mellem {0} og {1}",
"DynamicFormNumberValueMustBeLowerThan": "Værdien skal være lavere end {0}",
"DynamicFormChooseFileLabel": "File",
"DynamicFormChooseFileButtonText": "Select file",
"DynamicFormRequiredFileMessage": "File is required.",
"customDisplayName": "Brug denne placering:",
"ListItemCommentDIalogDeleteSubText": "Er du sikker på, at du vil slette denne kommentar?",
"ListItemCommentsDialogDeleteTitle": "Bekræft kommentar til sletning",
Expand Down
Loading