Skip to content

Commit 2648fbb

Browse files
GuidoZamIRRDCjoaojmendes
authored
Added file handling in DynamicForm (#1625)
* Bugfix for issue 1568 Dynamic Form accessed TaxonomyFieldTypeMulti without considering sub-array "results". * Added file handling in DynamicForm * Updated pt-pt localization * Changed file upload * Removed file selection in edit mode * Updated loc files and removed unused props * Updated IDynamicFormProps * Added check for content type * Improved enableFileSelection property description * Update DynamicForm.md * Added DynamicFormWithFileSelection.png * Update DynamicForm.tsx * Small fixes * Updated DynamicForm doc --------- Co-authored-by: IRRDC <[email protected]> Co-authored-by: João Mendes <[email protected]>
1 parent 15b93ab commit 2648fbb

37 files changed

+308
-27
lines changed
16.1 KB
Loading

docs/documentation/docs/controls/DynamicForm.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ import { DynamicForm } from "@pnp/spfx-controls-react/lib/DynamicForm";
2626
```
2727
![DynamicForm](../assets/DynamicForm.png)
2828

29+
## File selection
30+
31+
To upload a file when creating a new document in a document library you need to specify:
32+
- enableFileSelection: Set this parameter to true to enable file selection.
33+
- contentTypeId: This parameter specifies the target content type ID of the document you are creating.
34+
- supportedFileExtensions: This parameter is optional and is used to specify the supported file extensions if they are different from the default ones.
35+
36+
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.
37+
38+
![DynamicFormWithFileSelection](../assets/DynamicFormWithFileSelection.png)
39+
2940
## Implementation
3041

3142
The `DynamicForm` can be configured with the following properties:
@@ -38,13 +49,15 @@ The `DynamicForm` can be configured with the following properties:
3849
| contentTypeId | string | no | content type ID |
3950
| disabled | boolean | no | Allows form to be disabled. Default value is `false`|
4051
| disabledFields | string[] | no | InternalName of fields that should be disabled. Default value is `false`|
52+
| 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`|
4153
| hiddenFields | string[] | no | InternalName of fields that should be hidden. Default value is `false`|
4254
| onListItemLoaded | (listItemData: any) => Promise&lt;void&gt; | no | List item loaded handler. Allows to access list item information after it's loaded.|
4355
| onBeforeSubmit | (listItemData: any) => Promise&lt;boolean&gt; | no | Before submit handler. Allows to modify the object to be submitted or cancel the submission. To cancel, return `true`.|
4456
| onSubmitted | (listItemData: any, listItem?: IItem) => void | no | Method that returns listItem data JSON object and PnPJS list item instance (`IItem`). |
4557
| onSubmitError | (listItemData: any, error: Error) => void | no | Handler of submission error. |
4658
| onCancelled | () => void | no | Handler when form has been cancelled. |
4759
| returnListItemInstanceOnSubmit | boolean | no | Specifies if `onSubmitted` event should pass PnPJS list item (`IItem`) as a second parameter. Default - `true` |
60+
| 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"]`. |
4861
| webAbsoluteUrl | string | no | Absolute Web Url of target site (user requires permissions). |
4962
| 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. |
5063
| respectEtag | boolean | no | Specifies if the form should respect the ETag of the item. Default - `true` |

src/controls/dynamicForm/DynamicForm.module.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,8 @@
147147
}
148148
}
149149
}
150+
151+
.selectedFileContainer {
152+
display: flex;
153+
margin: 10px 0px;
154+
}

src/controls/dynamicForm/DynamicForm.tsx

Lines changed: 180 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IStackTokens, Stack } from "@fluentui/react/lib/Stack";
1212
import * as React from "react";
1313
import { IUploadImageResult } from "../../common/SPEntities";
1414
import SPservice from "../../services/SPService";
15-
import { IFilePickerResult } from "../filePicker";
15+
import { FilePicker, IFilePickerResult } from "../filePicker";
1616
import { DynamicField } from "./dynamicField";
1717
import {
1818
DateFormat,
@@ -27,6 +27,7 @@ import {
2727
DialogFooter,
2828
DialogType,
2929
} from "@fluentui/react/lib/Dialog";
30+
import { Icon } from 'office-ui-fabric-react';
3031

3132
import "@pnp/sp/lists";
3233
import "@pnp/sp/content-types";
@@ -109,6 +110,11 @@ export class DynamicForm extends React.Component<
109110
</div>
110111
) : (
111112
<div>
113+
{this.props.enableFileSelection === true &&
114+
this.props.listItemId === undefined &&
115+
this.props.contentTypeId !== undefined &&
116+
this.props.contentTypeId.startsWith("0x0101") &&
117+
this.renderFileSelectionControl()}
112118
{fieldCollection.map((v, i) => {
113119
if (
114120
fieldOverrides &&
@@ -182,6 +188,9 @@ export class DynamicForm extends React.Component<
182188
onSubmitted,
183189
onBeforeSubmit,
184190
onSubmitError,
191+
enableFileSelection,
192+
validationErrorDialogProps,
193+
returnListItemInstanceOnSubmit
185194
} = this.props;
186195

187196
try {
@@ -218,11 +227,22 @@ export class DynamicForm extends React.Component<
218227
}
219228
}
220229
});
230+
221231
if (shouldBeReturnBack) {
222232
this.setState({
223233
fieldCollection: fields,
224234
isValidationErrorDialogOpen:
225-
this.props.validationErrorDialogProps
235+
validationErrorDialogProps
236+
?.showDialogOnValidationError === true,
237+
});
238+
return;
239+
}
240+
241+
if (enableFileSelection === true && this.state.selectedFile === undefined && this.props.listItemId === undefined) {
242+
this.setState({
243+
missingSelectedFile: true,
244+
isValidationErrorDialogOpen:
245+
validationErrorDialogProps
226246
?.showDialogOnValidationError === true,
227247
});
228248
return;
@@ -322,7 +342,7 @@ export class DynamicForm extends React.Component<
322342
if (onSubmitted) {
323343
onSubmitted(
324344
iur.data,
325-
this.props.returnListItemInstanceOnSubmit !== false
345+
returnListItemInstanceOnSubmit !== false
326346
? iur.item
327347
: undefined
328348
);
@@ -338,28 +358,33 @@ export class DynamicForm extends React.Component<
338358
else if (
339359
contentTypeId === undefined ||
340360
contentTypeId === "" ||
341-
!contentTypeId.startsWith("0x0120")||
342-
contentTypeId.startsWith("0x01")
361+
(!contentTypeId.startsWith("0x0120") &&
362+
contentTypeId.startsWith("0x01"))
343363
) {
344-
// We are adding a new list item
345-
try {
346-
const contentTypeIdField = "ContentTypeId";
347-
//check if item contenttype is passed, then update the object with content type id, else, pass the object
348-
if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId;
349-
const iar = await sp.web.lists.getById(listId).items.add(objects);
350-
if (onSubmitted) {
351-
onSubmitted(
352-
iar.data,
353-
this.props.returnListItemInstanceOnSubmit !== false
354-
? iar.item
355-
: undefined
356-
);
357-
}
358-
} catch (error) {
359-
if (onSubmitError) {
360-
onSubmitError(objects, error);
364+
if (contentTypeId === undefined || enableFileSelection === true) {
365+
await this.addFileToLibrary(objects);
366+
}
367+
else {
368+
// We are adding a new list item
369+
try {
370+
const contentTypeIdField = "ContentTypeId";
371+
//check if item contenttype is passed, then update the object with content type id, else, pass the object
372+
if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId;
373+
const iar = await sp.web.lists.getById(listId).items.add(objects);
374+
if (onSubmitted) {
375+
onSubmitted(
376+
iar.data,
377+
this.props.returnListItemInstanceOnSubmit !== false
378+
? iar.item
379+
: undefined
380+
);
381+
}
382+
} catch (error) {
383+
if (onSubmitError) {
384+
onSubmitError(objects, error);
385+
}
386+
console.log("Error", error);
361387
}
362-
console.log("Error", error);
363388
}
364389
}
365390
else if (contentTypeId.startsWith("0x0120")) {
@@ -408,6 +433,9 @@ export class DynamicForm extends React.Component<
408433
}
409434
console.log("Error", error);
410435
}
436+
} else if (contentTypeId.startsWith("0x01") && enableFileSelection === true) {
437+
// We are adding a folder or a Document Set
438+
await this.addFileToLibrary(objects);
411439
}
412440

413441
this.setState({
@@ -422,6 +450,64 @@ export class DynamicForm extends React.Component<
422450
}
423451
};
424452

453+
private addFileToLibrary = async (objects: {}): Promise<void> => {
454+
const {
455+
selectedFile
456+
} = this.state;
457+
458+
const {
459+
listId,
460+
contentTypeId,
461+
onSubmitted,
462+
onSubmitError,
463+
returnListItemInstanceOnSubmit
464+
} = this.props;
465+
466+
try {
467+
const idField = "ID";
468+
const contentTypeIdField = "ContentTypeId";
469+
470+
const library = await sp.web.lists.getById(listId);
471+
const itemTitle =
472+
selectedFile !== undefined && selectedFile.fileName !== undefined && selectedFile.fileName !== ""
473+
? (selectedFile.fileName as string).replace(
474+
/["|*|:|<|>|?|/|\\||]/g,
475+
"_"
476+
) // Replace not allowed chars in folder name
477+
: ""; // Empty string will be replaced by SPO with Folder Item ID
478+
479+
const fileCreatedResult = await library.rootFolder.files.addChunked(encodeURI(itemTitle), await selectedFile.downloadFileContent());
480+
const fields = await fileCreatedResult.file.listItemAllFields();
481+
482+
if (fields[idField]) {
483+
// Read the ID of the just created folder or Document Set
484+
const folderId = fields[idField];
485+
486+
// Set the content type ID for the target item
487+
objects[contentTypeIdField] = contentTypeId;
488+
// Update the just created folder or Document Set
489+
const iur = await library.items.getById(folderId).update(objects);
490+
if (onSubmitted) {
491+
onSubmitted(
492+
iur.data,
493+
returnListItemInstanceOnSubmit !== false
494+
? iur.item
495+
: undefined
496+
);
497+
}
498+
} else {
499+
throw new Error(
500+
"Unable to read the ID of the just created folder or Document Set"
501+
);
502+
}
503+
} catch (error) {
504+
if (onSubmitError) {
505+
onSubmitError(objects, error);
506+
}
507+
console.log("Error", error);
508+
}
509+
}
510+
425511
// trigger when the user change any value in the form
426512
private onChange = async (
427513
internalName: string,
@@ -871,6 +957,77 @@ export class DynamicForm extends React.Component<
871957
return errorMessage;
872958
};
873959

960+
private renderFileSelectionControl = (): React.ReactElement => {
961+
const {
962+
selectedFile,
963+
missingSelectedFile
964+
} = this.state;
965+
966+
const labelEl = <label className={styles.fieldRequired + ' ' + styles.fieldLabel}>{strings.DynamicFormChooseFileLabel}</label>;
967+
968+
return <div>
969+
<div className={styles.titleContainer}>
970+
<Icon className={styles.fieldIcon} iconName={"DocumentSearch"} />
971+
{labelEl}
972+
</div>
973+
<FilePicker
974+
buttonLabel={strings.DynamicFormChooseFileButtonText}
975+
accepts={this.props.supportedFileExtensions ? this.props.supportedFileExtensions : [".docx", ".doc", ".pptx", ".ppt", ".xlsx", ".xls", ".pdf"]}
976+
onSave={(filePickerResult: IFilePickerResult[]) => {
977+
if (filePickerResult.length === 1) {
978+
this.setState({
979+
selectedFile: filePickerResult[0],
980+
missingSelectedFile: false
981+
});
982+
}
983+
else {
984+
this.setState({
985+
missingSelectedFile: true
986+
});
987+
}
988+
}}
989+
required={true}
990+
context={this.props.context}
991+
hideWebSearchTab={true}
992+
hideStockImages={true}
993+
hideLocalMultipleUploadTab={true}
994+
hideLinkUploadTab={true}
995+
hideSiteFilesTab={true}
996+
checkIfFileExists={true}
997+
/>
998+
{selectedFile && <div className={styles.selectedFileContainer}>
999+
<Icon iconName={this.getFileIconFromExtension()} />
1000+
{selectedFile.fileName}
1001+
</div>}
1002+
{missingSelectedFile === true &&
1003+
<div className={styles.errormessage}>{strings.DynamicFormRequiredFileMessage}</div>}
1004+
</div>;
1005+
}
1006+
1007+
private getFileIconFromExtension = (): string => {
1008+
const fileExtension = this.state.selectedFile.fileName.split('.').pop();
1009+
switch (fileExtension) {
1010+
case 'pdf':
1011+
return 'PDF';
1012+
case 'docx':
1013+
case 'doc':
1014+
return 'WordDocument';
1015+
case 'pptx':
1016+
case 'ppt':
1017+
return 'PowerPointDocument';
1018+
case 'xlsx':
1019+
case 'xls':
1020+
return 'ExcelDocument';
1021+
case 'jpg':
1022+
case 'jpeg':
1023+
case 'png':
1024+
case 'gif':
1025+
return 'FileImage';
1026+
default:
1027+
return 'Document';
1028+
}
1029+
}
1030+
8741031
private isEmptyNumOrString(value: string | number): boolean {
8751032
if ((value?.toString().trim().length || 0) === 0) return true;
8761033
}

src/controls/dynamicForm/IDynamicFormProps.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,17 @@ export interface IDynamicFormProps {
8282
* Specify validation error dialog properties
8383
*/
8484
validationErrorDialogProps?: IValidationErrorDialogProps;
85+
86+
/**
87+
* Specify if the form should support the creation of a new list item in a document library attaching a file to it.
88+
* This option is only available for document libraries and works only when the contentTypeId is specified and has a base type of type Document.
89+
* Default - false
90+
*/
91+
enableFileSelection?: boolean;
92+
93+
/**
94+
* Specify the supported file extensions for the file picker. Default - "docx", "doc", "pptx", "ppt", "xlsx", "xls", "pdf"
95+
* Only used when enableFileSelection is true
96+
*/
97+
supportedFileExtensions?: string[];
8598
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
21
import { IInstalledLanguageInfo } from '@pnp/sp/regional-settings';
32
import { IDynamicFieldProps } from './dynamicField/IDynamicFieldProps';
3+
import { IFilePickerResult } from "../filePicker";
4+
45
export interface IDynamicFormState {
56
fieldCollection: IDynamicFieldProps[];
67
installedLanguages?: IInstalledLanguageInfo[];
78
isSaving?: boolean;
89
etag?: string;
910
isValidationErrorDialogOpen: boolean;
11+
selectedFile?: IFilePickerResult;
12+
missingSelectedFile?: boolean;
1013
}
11-
12-
13-

src/loc/bg-bg.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ define([], () => {
370370
"DynamicFormNumberValueMustBeGreaterThan": "Стойността трябва да е по-голяма от {0}",
371371
"DynamicFormNumberValueMustBeBetween": "Стойността трябва да е между {0} и {1}",
372372
"DynamicFormNumberValueMustBeLowerThan": "Стойността трябва да е по-ниска от {0}",
373+
"DynamicFormChooseFileLabel": "File",
374+
"DynamicFormChooseFileButtonText": "Select file",
375+
"DynamicFormRequiredFileMessage": "File is required.",
373376
"customDisplayName": "Използвайте това местоположение:",
374377
"ListItemCommentDIalogDeleteSubText": "Наистина ли искате да изтриете този коментар?",
375378
"ListItemCommentsDialogDeleteTitle": "Потвърдете Изтриване на коментар",

src/loc/ca-es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ define([], () => {
370370
"DynamicFormNumberValueMustBeGreaterThan": "El valor ha de ser superior a {0}",
371371
"DynamicFormNumberValueMustBeBetween": "El valor ha d'estar entre {0} i {1}",
372372
"DynamicFormNumberValueMustBeLowerThan": "El valor ha de ser inferior a {0}",
373+
"DynamicFormChooseFileLabel": "File",
374+
"DynamicFormChooseFileButtonText": "Select file",
375+
"DynamicFormRequiredFileMessage": "File is required.",
373376
"customDisplayName": "Utilitzeu aquesta ubicació:",
374377
"ListItemCommentDIalogDeleteSubText": "Esteu segur que voleu suprimir aquest comentari?",
375378
"ListItemCommentsDialogDeleteTitle": "Confirmació de la supressió del comentari",

src/loc/da-dk.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ define([], () => {
370370
"DynamicFormNumberValueMustBeGreaterThan": "Værdien skal være større end {0}",
371371
"DynamicFormNumberValueMustBeBetween": "Værdien skal være mellem {0} og {1}",
372372
"DynamicFormNumberValueMustBeLowerThan": "Værdien skal være lavere end {0}",
373+
"DynamicFormChooseFileLabel": "File",
374+
"DynamicFormChooseFileButtonText": "Select file",
375+
"DynamicFormRequiredFileMessage": "File is required.",
373376
"customDisplayName": "Brug denne placering:",
374377
"ListItemCommentDIalogDeleteSubText": "Er du sikker på, at du vil slette denne kommentar?",
375378
"ListItemCommentsDialogDeleteTitle": "Bekræft kommentar til sletning",

src/loc/de-de.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ define([], () => {
370370
"DynamicFormNumberValueMustBeGreaterThan": "Der Wert muss größer als {0} sein.",
371371
"DynamicFormNumberValueMustBeBetween": "Der Wert muss zwischen {0} und {1} liegen.",
372372
"DynamicFormNumberValueMustBeLowerThan": "Der Wert muss niedriger als {0} sein.",
373+
"DynamicFormChooseFileLabel": "File",
374+
"DynamicFormChooseFileButtonText": "Select file",
375+
"DynamicFormRequiredFileMessage": "File is required.",
373376
"customDisplayName": "Verwenden Sie diesen Speicherort:",
374377
"ListItemCommentDIalogDeleteSubText": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
375378
"ListItemCommentsDialogDeleteTitle": "Kommentar löschen bestätigen",

0 commit comments

Comments
 (0)