Attachment for react list form (#1167)
* #1 Add CSS classes for correct layout from Office UI Fabric * #1 - Removed intermediary component, which was only sending data * #1 - Add all the component methods, carried out some simple tests * #01 - Remove CSS styles from the FormField. Only date controls have those * #01 - Fix some minor issues * Add a lot about attachments * Fixed tha files with the same name cannot be added * Added attachments functionality * Fixed the bug with multiple attachments delete * Removed console.log lines Co-authored-by: SharePoint Student <student@CONTOSOLTD.LOCAL>
This commit is contained in:
parent
d159945fd5
commit
c837006bf6
File diff suppressed because it is too large
Load Diff
|
@ -1,43 +1,44 @@
|
||||||
{
|
{
|
||||||
"name": "react-form-webpart",
|
"name": "react-form-webpart",
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
},
|
},
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "gulp serve",
|
"serve": "gulp serve",
|
||||||
"build": "gulp bundle",
|
"build": "gulp bundle",
|
||||||
"clean": "gulp clean",
|
"clean": "gulp clean",
|
||||||
"test": "gulp test"
|
"test": "gulp test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/sp-core-library": "1.9.1",
|
"@microsoft/sp-core-library": "1.9.1",
|
||||||
"@microsoft/sp-lodash-subset": "1.9.1",
|
"@microsoft/sp-lodash-subset": "1.9.1",
|
||||||
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
||||||
"@microsoft/sp-webpart-base": "1.9.1",
|
"@microsoft/sp-webpart-base": "1.9.1",
|
||||||
"@tinymce/tinymce-react": "^3.0.1",
|
"@tinymce/tinymce-react": "^3.0.1",
|
||||||
"@types/es6-promise": "0.0.33",
|
"@types/es6-promise": "0.0.33",
|
||||||
"@types/react-dnd": "~2.0.34",
|
"@types/react-dnd": "~2.0.34",
|
||||||
"@types/webpack-env": "1.13.1",
|
"@types/webpack-env": "1.13.1",
|
||||||
"moment": "^2.24.0",
|
"@uifabric/icons": "^7.3.14",
|
||||||
"react-dnd": "~2.5.4",
|
"moment": "^2.24.0",
|
||||||
"react-dnd-html5-backend": "~2.5.4",
|
"react-dnd": "~2.5.4",
|
||||||
"react-html-parser": "^2.0.2",
|
"react-dnd-html5-backend": "~2.5.4",
|
||||||
"spfx-uifabric-themes": "~0.1.3",
|
"react-html-parser": "^2.0.2",
|
||||||
"tinymce": "^5.0.1"
|
"spfx-uifabric-themes": "~0.1.3",
|
||||||
},
|
"tinymce": "^5.0.1"
|
||||||
"devDependencies": {
|
},
|
||||||
"@microsoft/rush-stack-compiler-2.9": "^0.8.5",
|
"devDependencies": {
|
||||||
"@microsoft/sp-build-web": "1.9.1",
|
"@microsoft/rush-stack-compiler-2.9": "^0.8.5",
|
||||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
"@microsoft/sp-build-web": "1.9.1",
|
||||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
"@microsoft/sp-module-interfaces": "1.9.1",
|
||||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
"@microsoft/sp-tslint-rules": "1.9.1",
|
||||||
"@types/chai": "3.4.34",
|
"@microsoft/sp-webpart-workbench": "1.9.1",
|
||||||
"@types/mocha": "2.2.38",
|
"@types/chai": "3.4.34",
|
||||||
"ajv": "~5.2.2",
|
"@types/mocha": "2.2.38",
|
||||||
"gulp": "~3.9.1",
|
"ajv": "~5.2.2",
|
||||||
"tslint-microsoft-contrib": "5.0.0"
|
"gulp": "~3.9.1",
|
||||||
}
|
"tslint-microsoft-contrib": "5.0.0"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import * as strings from 'servicesStrings';
|
||||||
import { ControlMode } from '../datatypes/ControlMode';
|
import { ControlMode } from '../datatypes/ControlMode';
|
||||||
import { IFieldSchema, RenderListDataOptions } from './datatypes/RenderListData';
|
import { IFieldSchema, RenderListDataOptions } from './datatypes/RenderListData';
|
||||||
import { IListFormService } from './IListFormService';
|
import { IListFormService } from './IListFormService';
|
||||||
|
import { IAttachment } from '../../types/IAttachment';
|
||||||
|
|
||||||
|
|
||||||
export class ListFormService implements IListFormService {
|
export class ListFormService implements IListFormService {
|
||||||
|
|
||||||
|
@ -118,41 +120,45 @@ export class ListFormService implements IListFormService {
|
||||||
* @param originalData An object containing all the field values retrieved on loading from list item.
|
* @param originalData An object containing all the field values retrieved on loading from list item.
|
||||||
* @returns Promise object represents the updated or erroneous form field values.
|
* @returns Promise object represents the updated or erroneous form field values.
|
||||||
*/
|
*/
|
||||||
public updateItem(webUrl: string, listUrl: string, itemId: number,
|
public updateItem = async (webUrl: string, listUrl: string, itemId: number, fieldsSchema: IFieldSchema[], data: any, originalData: any) => {
|
||||||
fieldsSchema: IFieldSchema[], data: any, originalData: any): Promise<any> {
|
const httpClientOptions: ISPHttpClientOptions = {
|
||||||
return new Promise<any>((resolve, reject) => {
|
headers: {
|
||||||
const httpClientOptions: ISPHttpClientOptions = {
|
'Accept': 'application/json;odata=verbose',
|
||||||
headers: {
|
'Content-type': 'application/json;odata=verbose',
|
||||||
'Accept': 'application/json;odata=verbose',
|
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
|
||||||
'Content-type': 'application/json;odata=verbose',
|
'odata-version': '',
|
||||||
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
|
},
|
||||||
'odata-version': '',
|
};
|
||||||
},
|
const formValues = this.GetFormValues(fieldsSchema, data, originalData);
|
||||||
};
|
let createAttachmetns = this.GetAttachmentsCreate(data);
|
||||||
const formValues = this.GetFormValues(fieldsSchema, data, originalData);
|
let deleteAttachmetns = this.GetAttachmentsDelete(data, originalData);
|
||||||
|
httpClientOptions.body = JSON.stringify({
|
||||||
httpClientOptions.body = JSON.stringify({
|
bNewDocumentUpdate: false,
|
||||||
bNewDocumentUpdate: false,
|
checkInComment: null,
|
||||||
checkInComment: null,
|
formValues,
|
||||||
formValues,
|
|
||||||
});
|
|
||||||
const endpoint = `${webUrl}/_api/web/GetList(@listUrl)/items(@itemId)/ValidateUpdateListItem()`
|
|
||||||
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}&@itemId=%27${itemId}%27`;
|
|
||||||
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions)
|
|
||||||
.then((response: SPHttpClientResponse) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
reject(this.getErrorMessage(webUrl, response));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((respData) => {
|
|
||||||
resolve(respData.d.ValidateUpdateListItem.results);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
reject(this.getErrorMessage(webUrl, error));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
const endpoint = `${webUrl}/_api/web/GetList(@listUrl)/items(@itemId)/ValidateUpdateListItem()`
|
||||||
|
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}&@itemId=%27${itemId}%27`;
|
||||||
|
try {
|
||||||
|
let response = await this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
return this.getErrorMessage(webUrl, response);
|
||||||
|
}
|
||||||
|
let responseData = await response.json();
|
||||||
|
responseData.AttachmentResponse = [];
|
||||||
|
if (deleteAttachmetns.length > 0) {
|
||||||
|
let deleteResponse = await this.deleteAttachments(webUrl, listUrl, itemId, deleteAttachmetns);
|
||||||
|
responseData.AttachmentResponse.push(deleteResponse);
|
||||||
|
}
|
||||||
|
if (createAttachmetns.length > 0) {
|
||||||
|
let createResponse = await this.uploadAttachments(webUrl, listUrl, itemId, createAttachmetns);
|
||||||
|
responseData.AttachmentResponse.push(createResponse);
|
||||||
|
}
|
||||||
|
return responseData.d.ValidateUpdateListItem.results;
|
||||||
|
} catch (error) {
|
||||||
|
return this.getErrorMessage(webUrl, error);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -167,6 +173,7 @@ export class ListFormService implements IListFormService {
|
||||||
public createItem(webUrl: string, listUrl: string, fieldsSchema: IFieldSchema[], data: any): Promise<any> {
|
public createItem(webUrl: string, listUrl: string, fieldsSchema: IFieldSchema[], data: any): Promise<any> {
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
const formValues = this.GetFormValues(fieldsSchema, data, {});
|
const formValues = this.GetFormValues(fieldsSchema, data, {});
|
||||||
|
const formAttachmetns = this.GetAttachmentsCreate(data);
|
||||||
const httpClientOptions: ISPHttpClientOptions = {
|
const httpClientOptions: ISPHttpClientOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json;odata=verbose',
|
'Accept': 'application/json;odata=verbose',
|
||||||
|
@ -198,6 +205,20 @@ export class ListFormService implements IListFormService {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((respData) => {
|
.then((respData) => {
|
||||||
|
let itemId = respData.d.AddValidateUpdateItemUsingPath.results.find(item => {
|
||||||
|
return item.FieldName == "Id";
|
||||||
|
}).FieldValue;
|
||||||
|
//if there are attachments, we upload all of them.
|
||||||
|
if (formAttachmetns.length > 0) {
|
||||||
|
this.uploadAttachments(webUrl, listUrl, itemId, formAttachmetns)
|
||||||
|
.then((attachmentResponse) => {
|
||||||
|
respData.AttachmentResponse = attachmentResponse;
|
||||||
|
resolve(respData.d.AddValidateUpdateItemUsingPath.results);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(this.getErrorMessage(webUrl, error));
|
||||||
|
});
|
||||||
|
}
|
||||||
resolve(respData.d.AddValidateUpdateItemUsingPath.results);
|
resolve(respData.d.AddValidateUpdateItemUsingPath.results);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -206,6 +227,59 @@ export class ListFormService implements IListFormService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private uploadAttachments = async (webUrl: string, listUrl: string, itemId: number, attachments: any) => {
|
||||||
|
let responses = [];
|
||||||
|
for (var i = 0; i < attachments.length; i++) {
|
||||||
|
let attachment = attachments[i];
|
||||||
|
let httpClientOptions: ISPHttpClientOptions = {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json; odata=verbose",
|
||||||
|
"content-type": "application/json; odata=verbose",
|
||||||
|
"content-length": attachment.buffer.byteLength,
|
||||||
|
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
|
||||||
|
'odata-version': '',
|
||||||
|
},
|
||||||
|
body: attachment.buffer,
|
||||||
|
};
|
||||||
|
let endpoint = `${webUrl}/_api/web/GetList(@listUrl)/items(@itemId)/AttachmentFiles/add(FileName='${attachment.fileName}')`
|
||||||
|
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}&@itemId=%27${itemId}%27`;
|
||||||
|
try {
|
||||||
|
let response = await this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions);
|
||||||
|
if (response.ok) {
|
||||||
|
let respJson = await response.json();
|
||||||
|
responses.push(respJson);
|
||||||
|
} else {
|
||||||
|
return this.getErrorMessage(webUrl, response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
private deleteAttachments = async (webUrl: string, listUrl: string, itemId: number, attachments: string[]) => {
|
||||||
|
let responses = [];
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
let httpClientOptions: ISPHttpClientOptions = {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json; odata=verbose",
|
||||||
|
"content-type": "application/json; odata=verbose",
|
||||||
|
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
|
||||||
|
"X-HTTP-Method": "DELETE",
|
||||||
|
'odata-version': '',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let endpoint = `${webUrl}/_api/web/GetList(@listUrl)/items(@itemId)/AttachmentFiles/getByFileName('${attachment}')`
|
||||||
|
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}&@itemId=%27${itemId}%27`;
|
||||||
|
try {
|
||||||
|
await this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions);
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
private GetFormValues(fieldsSchema: IFieldSchema[], data: any, originalData: any)
|
private GetFormValues(fieldsSchema: IFieldSchema[], data: any, originalData: any)
|
||||||
: Array<{ FieldName: string, FieldValue: any, HasException: boolean, ErrorMessage: string }> {
|
: Array<{ FieldName: string, FieldValue: any, HasException: boolean, ErrorMessage: string }> {
|
||||||
return fieldsSchema.filter(
|
return fieldsSchema.filter(
|
||||||
|
@ -214,8 +288,8 @@ export class ListFormService implements IListFormService {
|
||||||
&& (field.InternalName in data)
|
&& (field.InternalName in data)
|
||||||
&& (data[field.InternalName] !== null)
|
&& (data[field.InternalName] !== null)
|
||||||
&& (data[field.InternalName] !== originalData[field.InternalName])
|
&& (data[field.InternalName] !== originalData[field.InternalName])
|
||||||
),
|
&& (field.InternalName != "Attachments")
|
||||||
)
|
))
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
return {
|
return {
|
||||||
ErrorMessage: null,
|
ErrorMessage: null,
|
||||||
|
@ -223,8 +297,37 @@ export class ListFormService implements IListFormService {
|
||||||
FieldValue: data[field.InternalName],
|
FieldValue: data[field.InternalName],
|
||||||
HasException: false,
|
HasException: false,
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
private GetAttachmentsCreate(data: any)
|
||||||
|
: Array<{ buffer: any, bufferLength: number, fileName: string }> {
|
||||||
|
var results = new Array<{ buffer: any, bufferLength: number, fileName: string }>();
|
||||||
|
if (data.Attachments && data.Attachments.length > 0) {
|
||||||
|
results = data.Attachments
|
||||||
|
.filter((attachment: IAttachment) => typeof attachment.AttachmentId == "undefined")
|
||||||
|
.map((attachment: IAttachment) => {
|
||||||
|
return {
|
||||||
|
buffer: attachment.FileBuffer,
|
||||||
|
bufferLength: attachment.FileBuffer.byteLength,
|
||||||
|
fileName: attachment.FileName
|
||||||
|
} as { buffer: any, bufferLength: number, fileName: string };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
private GetAttachmentsDelete(data: any, originalData: any)
|
||||||
|
: Array<string> {
|
||||||
|
let results = new Array<string>();
|
||||||
|
|
||||||
|
let newAttachments = (typeof data.Attachments.Attachments == "undefined") ? data.Attachments : data.Attachments.Attachments;
|
||||||
|
if (originalData && originalData.Attachments && originalData.Attachments.Attachments) {
|
||||||
|
let filtered = originalData.Attachments.Attachments
|
||||||
|
.filter((originalAttachment: IAttachment) =>
|
||||||
|
!newAttachments.some((attachment: IAttachment) => attachment.FileName == originalAttachment.FileName)
|
||||||
|
);
|
||||||
|
results = filtered.map((originalAttachment: IAttachment) => originalAttachment.FileName);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -99,6 +99,7 @@ export interface IFieldSchema {
|
||||||
Disable?: boolean;
|
Disable?: boolean;
|
||||||
WebServiceUrl: string;
|
WebServiceUrl: string;
|
||||||
HiddenListInternalName: string;
|
HiddenListInternalName: string;
|
||||||
|
HoursOptions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFormSchema {
|
export interface IFormSchema {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface IAttachment {
|
||||||
|
AttachmentId?: string;
|
||||||
|
FileName: string;
|
||||||
|
FileBuffer?: ArrayBuffer;
|
||||||
|
RedirectUrl?: string;
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import {
|
||||||
IPropertyPaneConfiguration, PropertyPaneDropdown,
|
IPropertyPaneConfiguration, PropertyPaneDropdown,
|
||||||
PropertyPaneTextField, PropertyPaneToggle, IPropertyPaneField
|
PropertyPaneTextField, PropertyPaneToggle, IPropertyPaneField
|
||||||
} from "@microsoft/sp-property-pane";
|
} from "@microsoft/sp-property-pane";
|
||||||
|
import { initializeIcons } from '@uifabric/icons';
|
||||||
|
|
||||||
import * as strings from 'ListFormWebPartStrings';
|
import * as strings from 'ListFormWebPartStrings';
|
||||||
import ListForm from './components/ListForm';
|
import ListForm from './components/ListForm';
|
||||||
|
@ -23,6 +24,7 @@ import { update, get } from '@microsoft/sp-lodash-subset';
|
||||||
import { ListService } from '../../common/services/ListService';
|
import { ListService } from '../../common/services/ListService';
|
||||||
import { ControlMode } from '../../common/datatypes/ControlMode';
|
import { ControlMode } from '../../common/datatypes/ControlMode';
|
||||||
|
|
||||||
|
initializeIcons();
|
||||||
|
|
||||||
export default class ListFormWebPart extends BaseClientSideWebPart<IListFormWebPartProps> {
|
export default class ListFormWebPart extends BaseClientSideWebPart<IListFormWebPartProps> {
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import HTML5Backend from 'react-dnd-html5-backend';
|
||||||
import * as strings from 'ListFormStrings';
|
import * as strings from 'ListFormStrings';
|
||||||
|
|
||||||
import styles from './ListForm.module.scss';
|
import styles from './ListForm.module.scss';
|
||||||
|
import { Validate } from '@microsoft/sp-core-library';
|
||||||
|
|
||||||
/*************************************************************************************
|
/*************************************************************************************
|
||||||
* React Component to render a SharePoint list form on any page.
|
* React Component to render a SharePoint list form on any page.
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Link, Label } from 'office-ui-fabric-react';
|
||||||
|
import { Separator } from 'office-ui-fabric-react/lib/Separator';
|
||||||
|
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||||
|
import { mergeStyles } from 'office-ui-fabric-react/lib/Styling';
|
||||||
|
import { IconNames } from '@uifabric/icons';
|
||||||
|
import { IAttachment } from "../../../../types/IAttachment";
|
||||||
|
import { ControlMode } from '../../../../common/datatypes/ControlMode';
|
||||||
|
|
||||||
|
interface IAttachmentProps {
|
||||||
|
controlMode: ControlMode;
|
||||||
|
value: any;
|
||||||
|
valueChanged(newValue: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttachmentState {
|
||||||
|
waitingImageUpload: boolean;
|
||||||
|
fileBuffer: any;
|
||||||
|
hideDialog: boolean;
|
||||||
|
attachments: IAttachment[];
|
||||||
|
}
|
||||||
|
export class AttachmentRender extends React.Component<IAttachmentProps, IAttachmentState> {
|
||||||
|
private inputRef;
|
||||||
|
constructor(props: IAttachmentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
waitingImageUpload: null,
|
||||||
|
fileBuffer: null,
|
||||||
|
hideDialog: true,
|
||||||
|
attachments: new Array<IAttachment>()
|
||||||
|
};
|
||||||
|
this._handleFileSelect = this._handleFileSelect.bind(this);
|
||||||
|
this._handleFileInputChange = this._handleFileInputChange.bind(this);
|
||||||
|
this._deleteFileItem = this._deleteFileItem.bind(this);
|
||||||
|
this.inputRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IAttachmentProps, prevState: IAttachmentState) {
|
||||||
|
//Component Value property got updated from List State
|
||||||
|
if (this.props.value
|
||||||
|
&& this.props.value.Attachments
|
||||||
|
&& this.props.value.Attachments.length > 0
|
||||||
|
&& this.state.attachments.length == 0
|
||||||
|
&& prevState.attachments.length == 0) {
|
||||||
|
this.setState({ attachments: this.props.value.Attachments });
|
||||||
|
}
|
||||||
|
if (this.state.attachments.length < prevState.attachments.length) {
|
||||||
|
this.props.valueChanged(this.state.attachments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this._renderFilesList()}
|
||||||
|
{this.props.controlMode != ControlMode.Display &&
|
||||||
|
<React.Fragment>
|
||||||
|
<Separator />
|
||||||
|
<Link onClick={this._handleFileSelect}>Add Attachment</Link>
|
||||||
|
<input id="inputFile1" ref={this.inputRef} type='file' onChange={this._handleFileInputChange} hidden />
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleFileSelect = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.inputRef.current.click();
|
||||||
|
}
|
||||||
|
private _handleFileInputChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
event.persist();
|
||||||
|
if (!event.target || !event.target.files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let attachments = new Array<IAttachment>();
|
||||||
|
for (var i = 0; i < event.target.files.length; i++) {
|
||||||
|
let file = event.target.files.item(i);
|
||||||
|
if (!this.state.attachments.some(attachment => attachment.FileName == file.name)) {
|
||||||
|
let contentsAsBuffer: any = await this._getFileAsBuffer(file);
|
||||||
|
let attachment: IAttachment = {
|
||||||
|
FileBuffer: contentsAsBuffer,
|
||||||
|
FileName: file.name,
|
||||||
|
RedirectUrl: URL.createObjectURL(file)
|
||||||
|
};
|
||||||
|
attachments.push(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let allAttachments = this.state.attachments.concat(attachments);
|
||||||
|
this.setState({
|
||||||
|
attachments: allAttachments
|
||||||
|
});
|
||||||
|
this.props.valueChanged(allAttachments);
|
||||||
|
//Clear Input field
|
||||||
|
this.inputRef.current.value = '';
|
||||||
|
}
|
||||||
|
private _renderFilesList() {
|
||||||
|
return this.state.attachments.map((attachment: IAttachment, index: number) => {
|
||||||
|
let itemclass = mergeStyles({
|
||||||
|
lineHeight: "32px"
|
||||||
|
});
|
||||||
|
let link = attachment.RedirectUrl ?
|
||||||
|
<Link className={itemclass} target="_blank" href={attachment.RedirectUrl.replace("1https://", "https://")}>{attachment.FileName} </Link> :
|
||||||
|
<Label className={itemclass}>{attachment.FileName}</Label>;
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{link}
|
||||||
|
{this.props.controlMode != ControlMode.Display &&
|
||||||
|
<IconButton iconProps={{ iconName: IconNames.Delete }} onClick={() => this._deleteFileItem(index)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private _deleteFileItem(index: number): void {
|
||||||
|
let attachments = new Array().concat(this.state.attachments);
|
||||||
|
attachments.splice(index, 1);
|
||||||
|
this.setState({ attachments });
|
||||||
|
}
|
||||||
|
private _getFileAsBuffer = inputFile => {
|
||||||
|
const temporaryFileReader = new FileReader();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
temporaryFileReader.onerror = () => {
|
||||||
|
temporaryFileReader.abort();
|
||||||
|
reject(new DOMException("Problem parsing input file."));
|
||||||
|
};
|
||||||
|
temporaryFileReader.onload = () => {
|
||||||
|
resolve(temporaryFileReader.result);
|
||||||
|
};
|
||||||
|
temporaryFileReader.readAsArrayBuffer(inputFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,144 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
import { css } from 'office-ui-fabric-react/lib/Utilities';
|
||||||
import { DatePicker, DayOfWeek, IDatePickerProps, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
|
import { DatePicker, DayOfWeek, IDatePickerProps, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
|
||||||
|
import { ComboBox, IComboBoxOption, IComboBox } from 'office-ui-fabric-react/lib/ComboBox';
|
||||||
|
|
||||||
import * as strings from 'FormFieldStrings';
|
import * as strings from 'FormFieldStrings';
|
||||||
|
import { IFieldSchema } from '../../../../common/services/datatypes/RenderListData';
|
||||||
|
|
||||||
|
|
||||||
export interface IDateFormFieldProps extends IDatePickerProps {
|
export interface IDateFormFieldProps extends IDatePickerProps {
|
||||||
locale: string;
|
locale: string;
|
||||||
|
fieldSchema: IFieldSchema;
|
||||||
|
valueChanged(newValue: any): void;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
export interface IDateFormFieldState {
|
||||||
|
date?: Date;
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DateFormField extends React.Component<IDateFormFieldProps> {
|
export default class DateFormField extends React.Component<IDateFormFieldProps, IDateFormFieldState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this._createComboBoxHours = this._createComboBoxHours.bind(this);
|
||||||
|
this._createComboBoxMinutes = this._createComboBoxMinutes.bind(this);
|
||||||
|
this.state = {
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IDateFormFieldProps, prevState: IDateFormFieldState) {
|
||||||
|
//Component Value property got updated from List State
|
||||||
|
if (this.props.value && prevProps.value != this.props.value) {
|
||||||
|
console.log("Component Value property got updated from List State");
|
||||||
|
let momentDate = moment(this.props.value);
|
||||||
|
this.setState({
|
||||||
|
hours: momentDate.hour(),
|
||||||
|
minutes: momentDate.minute(),
|
||||||
|
date: momentDate.toDate()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Component value updated
|
||||||
|
if (this.state.date && this.state.date != prevState.date) {
|
||||||
|
let result = this.props.fieldSchema.DisplayFormat == 1 ?
|
||||||
|
this.state.date.toLocaleDateString(this.props.locale) + " " + this.state.date.toLocaleTimeString(this.props.locale, { hour: "2-digit", minute: "2-digit" }) : //Date + Time
|
||||||
|
this.state.date.toLocaleDateString(this.props.locale); //Only date
|
||||||
|
this.props.valueChanged(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<React.Fragment>
|
||||||
{...this.props}
|
<DatePicker
|
||||||
parseDateFromString={(dateStr: string) => new Date(Date.parse(dateStr))}
|
allowTextInput={this.props.allowTextInput}
|
||||||
formatDate={(date: Date) => (typeof date.toLocaleDateString === 'function') ? date.toLocaleDateString(this.props.locale) : ''}
|
ariaLabel={this.props.ariaLabel}
|
||||||
strings={strings}
|
className={css(this.props.className, this.props.fieldSchema.DisplayFormat == 1 ? "ms-sm12 ms-md12 ms-lg6 ms-xl8" : "ms-sm12")}
|
||||||
/>
|
firstDayOfWeek={this.props.firstDayOfWeek}
|
||||||
|
formatDate={(date: Date) => (typeof date.toLocaleDateString === 'function') ? date.toLocaleDateString(this.props.locale) : ''}
|
||||||
|
isRequired={this.props.isRequired}
|
||||||
|
onSelectDate={this._onSelectDate}
|
||||||
|
parseDateFromString={(dateStr: string) => new Date(Date.parse(dateStr))}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
strings={strings}
|
||||||
|
value={this.state.date}
|
||||||
|
|
||||||
|
/>
|
||||||
|
{this.props.fieldSchema.DisplayFormat == 1 &&
|
||||||
|
<React.Fragment>
|
||||||
|
<ComboBox
|
||||||
|
onChange={this._onHoursChanged}
|
||||||
|
selectedKey={this.state.hours}
|
||||||
|
allowFreeform
|
||||||
|
autoComplete="on"
|
||||||
|
persistMenu={true}
|
||||||
|
options={this._createComboBoxHours()}
|
||||||
|
className={css(this.props.className, "ms-sm6", "ms-md6", "ms-lg3", "ms-xl2")}
|
||||||
|
/>
|
||||||
|
<ComboBox
|
||||||
|
selectedKey={this.state.minutes}
|
||||||
|
onChange={this._onMinutesChanged}
|
||||||
|
allowFreeform
|
||||||
|
autoComplete="on"
|
||||||
|
persistMenu={true}
|
||||||
|
options={this._createComboBoxMinutes()}
|
||||||
|
className={css(this.props.className, "ms-sm6", "ms-md6", "ms-lg3", "ms-xl2")}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onSelectDate = (inputDate: Date | null | undefined): void => {
|
||||||
|
let date = inputDate ? moment(inputDate) : moment();
|
||||||
|
date.hour(this.state.hours);
|
||||||
|
date.minute(this.state.minutes);
|
||||||
|
this.setState({ date: date.toDate() });
|
||||||
|
}
|
||||||
|
private _onHoursChanged = (event: React.FormEvent<IComboBox>, option?: IComboBoxOption): void => {
|
||||||
|
if (option) {
|
||||||
|
let date = this.state.date ? moment(this.state.date) : moment();
|
||||||
|
let hours = parseInt(option.key.toString());
|
||||||
|
date.hour(hours);
|
||||||
|
this.setState({ date: date.toDate(), hours });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private _onMinutesChanged = (event: React.FormEvent<IComboBox>, option?: IComboBoxOption): void => {
|
||||||
|
if (option) {
|
||||||
|
let date = this.state.date ? moment(this.state.date) : moment();
|
||||||
|
let minutes = parseInt(option.key.toString());
|
||||||
|
date.minute(minutes);
|
||||||
|
this.setState({ date: date.toDate(), minutes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createComboBoxHours(): IComboBoxOption[] {
|
||||||
|
let results = new Array<IComboBoxOption>();
|
||||||
|
if (this.props.fieldSchema.HoursOptions) {
|
||||||
|
results = this.props.fieldSchema.HoursOptions.map((item, index) => {
|
||||||
|
return {
|
||||||
|
key: index,
|
||||||
|
text: item
|
||||||
|
} as IComboBoxOption;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
private _createComboBoxMinutes(): IComboBoxOption[] {
|
||||||
|
let results = new Array<IComboBoxOption>();
|
||||||
|
for (var i = 0; i < 60; i++) {
|
||||||
|
results.push({
|
||||||
|
key: i,
|
||||||
|
text: ("00" + i).slice(-2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISPFormFieldProps } from './SPFormField';
|
||||||
|
import { AttachmentRender } from './AttachmentRender';
|
||||||
|
|
||||||
|
const SPAttachmentFormFieldEdit: React.SFC<ISPFormFieldProps> = (props, state) => {
|
||||||
|
if (props.fieldSchema.FieldType !== 'Attachment') {
|
||||||
|
return <AttachmentRender
|
||||||
|
controlMode={props.controlMode}
|
||||||
|
valueChanged={props.valueChanged}
|
||||||
|
value={props.value}
|
||||||
|
> </AttachmentRender>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SPAttachmentFormFieldEdit;
|
|
@ -11,16 +11,17 @@ import styles from './SPFormField.module.scss';
|
||||||
const SPFieldDateEdit: React.SFC<ISPFormFieldProps> = (props) => {
|
const SPFieldDateEdit: React.SFC<ISPFormFieldProps> = (props) => {
|
||||||
const locale = Locales[props.fieldSchema.LocaleId];
|
const locale = Locales[props.fieldSchema.LocaleId];
|
||||||
return <DateFormField
|
return <DateFormField
|
||||||
{...props.value && moment(props.value).isValid() ? { value: moment(props.value).toDate() } : {}}
|
className={css(styles.dateFormField, 'ard-dateFormField', 'ms-Grid-col')}
|
||||||
className={css(styles.dateFormField, 'ard-dateFormField')}
|
|
||||||
placeholder={strings.DateFormFieldPlaceholder}
|
placeholder={strings.DateFormFieldPlaceholder}
|
||||||
isRequired={props.fieldSchema.Required}
|
isRequired={props.fieldSchema.Required}
|
||||||
ariaLabel={props.fieldSchema.Title}
|
ariaLabel={props.fieldSchema.Title}
|
||||||
locale={Locales[locale]}
|
locale={locale}
|
||||||
firstDayOfWeek={props.fieldSchema.FirstDayOfWeek}
|
firstDayOfWeek={props.fieldSchema.FirstDayOfWeek}
|
||||||
allowTextInput
|
allowTextInput
|
||||||
onSelectDate={(date) => props.valueChanged(date.toLocaleDateString(locale))}
|
fieldSchema={props.fieldSchema}
|
||||||
|
value={props.value}
|
||||||
|
valueChanged={props.valueChanged}
|
||||||
/>;
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SPFieldDateEdit;
|
export default SPFieldDateEdit;
|
|
@ -21,6 +21,8 @@ import SPFieldLookupDisplay from './SPFieldLookupDisplay';
|
||||||
import SPFieldUserDisplay from './SPFieldUserDisplay';
|
import SPFieldUserDisplay from './SPFieldUserDisplay';
|
||||||
import SPFieldUrlDisplay from './SPFieldUrlDisplay';
|
import SPFieldUrlDisplay from './SPFieldUrlDisplay';
|
||||||
|
|
||||||
|
import SPAttachmentFormFieldEdit from './SPAttachmentFormFieldEdit';
|
||||||
|
|
||||||
import * as strings from 'FormFieldStrings';
|
import * as strings from 'FormFieldStrings';
|
||||||
import styles from './SPFormField.module.scss';
|
import styles from './SPFormField.module.scss';
|
||||||
|
|
||||||
|
@ -37,6 +39,7 @@ const EditFieldTypeMappings: { [fieldType: string]: React.StatelessComponent<ISP
|
||||||
DateTime: SPFieldDateEdit,
|
DateTime: SPFieldDateEdit,
|
||||||
Boolean: SPFieldBooleanEdit,
|
Boolean: SPFieldBooleanEdit,
|
||||||
File: SPFieldTextEdit,
|
File: SPFieldTextEdit,
|
||||||
|
Attachments: SPAttachmentFormFieldEdit
|
||||||
/* The following are known but unsupported types as of now:
|
/* The following are known but unsupported types as of now:
|
||||||
User: null,
|
User: null,
|
||||||
UserMulti: null,
|
UserMulti: null,
|
||||||
|
@ -91,7 +94,8 @@ const SPFormField: React.SFC<ISPFormFieldProps> = (props) => {
|
||||||
const fieldMapping = richText ? DisplayFieldTypeMappings['RichText'] : DisplayFieldTypeMappings[fieldType];
|
const fieldMapping = richText ? DisplayFieldTypeMappings['RichText'] : DisplayFieldTypeMappings[fieldType];
|
||||||
const childProps = fieldMapping.valuePreProcess ? { ...props, value: fieldMapping.valuePreProcess(props.value) } : props;
|
const childProps = fieldMapping.valuePreProcess ? { ...props, value: fieldMapping.valuePreProcess(props.value) } : props;
|
||||||
fieldControl = React.createElement(fieldMapping.component, childProps);
|
fieldControl = React.createElement(fieldMapping.component, childProps);
|
||||||
} else if (!props.hideIfFieldUnsupported) {
|
}
|
||||||
|
else if (!props.hideIfFieldUnsupported) {
|
||||||
const value = (props.value) ? ((typeof props.value === 'string') ? props.value : JSON.stringify(props.value)) : '';
|
const value = (props.value) ? ((typeof props.value === 'string') ? props.value : JSON.stringify(props.value)) : '';
|
||||||
fieldControl = <div className={`ard-${fieldType}field-display`}>
|
fieldControl = <div className={`ard-${fieldType}field-display`}>
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
|
|
|
@ -22,7 +22,25 @@ define([], function () {
|
||||||
prevYearAriaLabel: 'Go to previous year',
|
prevYearAriaLabel: 'Go to previous year',
|
||||||
nextYearAriaLabel: 'Go to next year',
|
nextYearAriaLabel: 'Go to next year',
|
||||||
isRequiredErrorMessage: 'This date is required.',
|
isRequiredErrorMessage: 'This date is required.',
|
||||||
invalidInputErrorMessage: 'Invalid date format.'
|
invalidInputErrorMessage: 'Invalid date format.',
|
||||||
|
|
||||||
|
//Attachment Data
|
||||||
|
SaveButtonText: 'Save',
|
||||||
|
'CancelButtonText': 'Cancel',
|
||||||
|
'AddNewFieldAction': 'Add a new field to form',
|
||||||
|
'LoadingFormIndicator': 'Loading the form...',
|
||||||
|
'ErrorLoadingSchema': 'Error loading schema for list ',
|
||||||
|
'ConfigureListMessage': 'Please configure a list in the web part\'s editor first.',
|
||||||
|
'RequiredValueMessage': 'Please enter a value!',
|
||||||
|
'ErrorLoadingData': 'Error loading data for item with ID ',
|
||||||
|
'ItemSavedSuccessfully': 'Item saved successfully.',
|
||||||
|
'FieldsErrorOnSaving': 'The item could not be saved. Please check detailed error messages on the fields below.',
|
||||||
|
'ErrorOnSavingListItem': 'Error on loading lists: ',
|
||||||
|
'MoveField': "Move field",
|
||||||
|
'RemoveField': "Remove field",
|
||||||
|
|
||||||
|
'RemoveButtonLabel': "Remove",
|
||||||
|
'ReplaceButtonLabel': "Replace",
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,12 @@ declare interface IFormFieldStrings {
|
||||||
nextMonthAriaLabel?: string;
|
nextMonthAriaLabel?: string;
|
||||||
prevYearAriaLabel?: string;
|
prevYearAriaLabel?: string;
|
||||||
nextYearAriaLabel?: string;
|
nextYearAriaLabel?: string;
|
||||||
|
|
||||||
|
//Attachment
|
||||||
|
RemoveButtonLabel: string;
|
||||||
|
ReplaceButtonLabel: string;
|
||||||
|
AttachmentTermsConditionTitleText: string;
|
||||||
|
AttachmentTermsConditionText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'FormFieldStrings' {
|
declare module 'FormFieldStrings' {
|
||||||
|
|
Loading…
Reference in New Issue