pnp / sp-dev-fx-property-controls

Reusable SPFx property pane controls - Open source initiative
https://pnp.github.io/sp-dev-fx-property-controls/
MIT License
237 stars 151 forks source link

SPfx FilePicker customCollectionFieldData Cannot Read Properties of Undefined #449

Open walleford opened 2 years ago

walleford commented 2 years ago

Thank you for reporting an issue, suggesting an enhancement, or asking a question. We appreciate your feedback - to help the team understand your needs please complete the below template to ensure we have the details to help. Thanks!

Please check out the documentation to see if your question is already addressed there. This will help us ensure our documentation is up to date.

Category

Version

Please specify what version of the library you are using: [ 3.6.0 ]

/ Question

I am using the customCollectionFieldData property control, and using a custom field to import the FilePicker into it. When trying to render multiple items, I get Cannot Read Properties of Undefined. I wanted to ask if anyone can help explain what I am doing wrong to render this, as I am the only person using SPfx at my job. I will paste the property pane code and the tsx code in here:

`export default class CallingCards extends React.Component<ICallingCardsProps, {}> { public render(): React.ReactElement {

    if (this.props.CallingCards && this.props.CallingCards.length > 0) {
        var contacts = this.props.CallingCards.map(el =>
            <div className={`${styles.tile}`}>
                <div className={`${styles.galleryframe}`}>
                    <img className={`${styles.galleryimg}`} key={el} src={this.props.CallingCards && this.props.CallingCards.length > 0 ? this.props.CallingCards[el].filePicker.fileAbsoluteUrl : ''} />
                    <div key={el}>{el.Name}</div>
                    <div key={el}>{el.Email}</div>
                    <div key={el}>{el.PhoneNumber}</div>
                </div>
            </div>
        )
    } else {
        return (
            <div className={`${styles.welcome}`}>Use property pane to create new Contact Cards!</div>
        )
    }

    return (
        <body style={{ height: '225px' }}>
                <div className={`${styles.grid}`}>
                    {contacts}
                </div>
        </body>
    );
}

}`

PropertyFieldCollectionData('CallingCards', { key: 'CallingCards', label: 'Contact Card Information', panelHeader: 'Contact Card Panel', manageBtnLabel: 'Manage Contact Cards', value: this.properties.CallingCards, fields: [ { id: 'Name', title: 'Contact Name', type: CustomCollectionFieldType.string, required: true }, { id: 'Email', title: 'Contact Email', type: CustomCollectionFieldType.string, required: true }, { id: 'PhoneNumber', title: 'Contact Phone Number', type: CustomCollectionFieldType.string, required: false }, { id: "filePicker", title: "Select File", type: CustomCollectionFieldType.custom, onCustomRender: (field, value, onUpdate, item, itemId, onError) => { return ( React.createElement(FilePicker, { key: itemId, context: this.context, buttonLabel: "Select File", onChange: (filePickerResult: IFilePickerResult[]) => { console.log('changing....', field); onUpdate(field.id, filePickerResult[0]); this.context.propertyPane.refresh(); this.render(); }, onSave: (filePickerResult: IFilePickerResult[]) => { console.log('saving....', field); if (filePickerResult && filePickerResult.length > 0) { console.log('filePickerResult && filePickerResult.length > 0'); if (filePickerResult[0].fileAbsoluteUrl == null) { console.log('filePickerResult[0].fileAbsoluteUrl == null'); filePickerResult[0].downloadFileContent().then(async r => { let fileresult = await this.web.getFolderByServerRelativeUrl(${this.context.pageContext.site.serverRelativeUrl}/SiteAssets/SitePages).files.addUsingPath(filePickerResult[0].fileName, r, true); filePickerResult[0].fileAbsoluteUrl =${this.context.pageContext.site.absoluteUrl}/SiteAssets/SitePages/${fileresult.data.Name}; console.log('saving....', filePickerResult[0]); onUpdate(field.id, filePickerResult[0]); this.context.propertyPane.refresh(); this.render(); }); } else { console.log('saving....', filePickerResult[0]); onUpdate(field.id, filePickerResult[0]); this.context.propertyPane.refresh(); this.render(); } } }, hideLocalUploadTab: false, hideLocalMultipleUploadTab: true, hideLinkUploadTab: false }) ); }, required: true }, ], disabled: false, }),

Thanks!

ghost commented 2 years ago

Thank you for reporting this issue. We will be triaging your incoming issue as soon as possible.

walleford commented 2 years ago

The code did not format properly:

import * as React from 'react';
import styles from './CallingCards.module.scss';
import { ICallingCardsProps } from './ICallingCardsProps';
import { escape } from '@microsoft/sp-lodash-subset';

export default class CallingCards extends React.Component<ICallingCardsProps, {}> {
    public render(): React.ReactElement<ICallingCardsProps> {

        if (this.props.CallingCards && this.props.CallingCards.length > 0) {
            var contacts = this.props.CallingCards.map(el =>
                <div className={`${styles.tile}`}>
                    <div className={`${styles.galleryframe}`}>
                        <img className={`${styles.galleryimg}`} key={el} src={this.props.CallingCards && this.props.CallingCards.length > 0 ? this.props.CallingCards[el].filePicker.fileAbsoluteUrl : ''} />
                        <div key={el}>{el.Name}</div>
                        <div key={el}>{el.Email}</div>
                        <div key={el}>{el.PhoneNumber}</div>
                    </div>
                </div>
            )
        } else {
            return (
                <div className={`${styles.welcome}`}>Use property pane to create new Contact Cards!</div>
            )
        }

        return (
            <body style={{ height: '225px' }}>
                    <div className={`${styles.grid}`}>
                        {contacts}
                    </div>
            </body>
        );
    }
}
PropertyFieldCollectionData('CallingCards', {
                                    key: 'CallingCards',
                                    label: 'Contact Card Information',
                                    panelHeader: 'Contact Card Panel',
                                    manageBtnLabel: 'Manage Contact Cards',
                                    value: this.properties.CallingCards,
                                    fields: [
                                        {
                                            id: 'Name',
                                            title: 'Contact Name',
                                            type: CustomCollectionFieldType.string,
                                            required: true
                                        },
                                        {
                                            id: 'Email',
                                            title: 'Contact Email',
                                            type: CustomCollectionFieldType.string,
                                            required: true
                                        },
                                        {
                                            id: 'PhoneNumber',
                                            title: 'Contact Phone Number',
                                            type: CustomCollectionFieldType.string,
                                            required: false
                                        },
                                        {
                                            id: "filePicker",
                                            title: "Select File",
                                            type: CustomCollectionFieldType.custom,
                                            onCustomRender: (field, value, onUpdate, item, itemId, onError) => {
                                                return (
                                                    React.createElement(FilePicker, {
                                                        key: itemId,
                                                        context: this.context,
                                                        buttonLabel: "Select File",
                                                        onChange: (filePickerResult: IFilePickerResult[]) => {
                                                            console.log('changing....', field);
                                                            onUpdate(field.id, filePickerResult[0]);
                                                            this.context.propertyPane.refresh();
                                                            this.render();
                                                        },
                                                        onSave: (filePickerResult: IFilePickerResult[]) => {
                                                            console.log('saving....', field);
                                                            if (filePickerResult && filePickerResult.length > 0) {
                                                                console.log('filePickerResult && filePickerResult.length > 0');
                                                                if (filePickerResult[0].fileAbsoluteUrl == null) {
                                                                    console.log('filePickerResult[0].fileAbsoluteUrl == null');
                                                                    filePickerResult[0].downloadFileContent().then(async r => {
                                                                        let fileresult = await this.web.getFolderByServerRelativeUrl(`${this.context.pageContext.site.serverRelativeUrl}/SiteAssets/SitePages`).files.addUsingPath(filePickerResult[0].fileName, r, true);
                                                                        filePickerResult[0].fileAbsoluteUrl = `${this.context.pageContext.site.absoluteUrl}/SiteAssets/SitePages/${fileresult.data.Name}`;
                                                                        console.log('saving....', filePickerResult[0]);
                                                                        onUpdate(field.id, filePickerResult[0]);
                                                                        this.context.propertyPane.refresh();
                                                                        this.render();
                                                                    });
                                                                } else {
                                                                    console.log('saving....', filePickerResult[0]);
                                                                    onUpdate(field.id, filePickerResult[0]);
                                                                    this.context.propertyPane.refresh();
                                                                    this.render();
                                                                }
                                                            }
                                                        },
                                                        hideLocalUploadTab: false,
                                                        hideLocalMultipleUploadTab: true,
                                                        hideLinkUploadTab: false
                                                    })
                                                );
                                            },
                                            required: true
                                        },
                                    ],
                                    disabled: false,
                                }),
joelfmrodrigues commented 2 years ago

@walleford I know it's a bit late so assume you have already resolved the problem? If not, are you able to debug and try to identify the line where the error happens?

walleford commented 2 years ago

@joelfmrodrigues Thank you for the response, I was able to figure this particular issue out, however, I am still struggling with getting uploaded pictures to render. I don't believe my onSave it saving the files to sharepoint so they can be rendered. Do you have any guidance on where I can figure out how to do this or know of what I can do?

joelfmrodrigues commented 2 years ago

Hi @walleford

Copying this from a recent project where we used the control, hope it helps

Control (onSave can also be declared as a separate function if you prefer):

<FilePicker
    label={!props.extended && strings.DocumentsLabel}
    buttonLabel={strings.AddDocumentsLabel}
    buttonIcon="Add"
    onSave={async (filesPicked: IFilePickerResult[]) => {
      for (const filePicked of filesPicked) {
        const filePickedContent = await filePicked.downloadFileContent();
        uploadFiles(filePickedContent, filePicked.fileName);
      }
    }}
    context={webPartContext}
    hideSiteFilesTab
    hideStockImages
    hideWebSearchTab
    hideOneDriveTab
    hideRecentTab
    hideLinkUploadTab
    hideOrganisationalAssetTab
    disabled={
      documentsQueryStatus.loading ||
      props.disabled
    }
  />

For uploading the content using PnPjs:


try {
      // small upload
      if (fileContent.size <= 10485760) {
        const uploaded = await sp.web.getFolderByServerRelativeUrl(FolderPath)
          .usingCaching()
          .files.add(fileName, fileContent, true);
        return Promise.resolve(true);
      }

      // large upload
      else {
        const uploaded = await sp.web.getFolderByServerRelativeUrl(FolderPath).usingCaching()
          .files.addChunked(
            fileName,
            fileContent,
            (data) => {
            },
            true
          );
        return Promise.resolve(true);
      }

    }
    catch (err) {
      // (error handling removed for simplicity)
      return Promise.resolve(false);
    }
walleford commented 2 years ago

Hello,

Thank you for that. Could you point me to the webpart utilizing it? I would like to see the code so I can understand better what is exactly happening.

joelfmrodrigues commented 2 years ago

Sorry, this is a private web part from a client, not a public one.

walleford commented 2 years ago

Ah okay I see. The onSave function looks like it is calling an uploadFiles function and passing the filePickedContent and filePicked.fileName. Would you be able to share that?

joelfmrodrigues commented 2 years ago

Both properties (file content and file name) are then used in the block of code that I shared below. In my case, the solution is very complex and there is a lot of business logic going on between the two functions to manipulate data, which is why I didn't include that here, but in your case, you can just create a function uploadFiles and use the second block of code there - adding any missing bits, like the FolderPath. Also don't forget to initialize PnPjs as per the library documentation

PS: where I have return Promise.resolve(true);, this was simplified without testing when copying here. In our case, we return a custom object with multiple properties that are required by our app so you can change it if needed or keep it as a boolean. Up to you

walleford commented 2 years ago

Hello,

I have set it up this way:

private async uploadFiles(fileContent, fileName, FolderPath) {
        try {
            if (fileContent.size <= 10485760) {
                // small upload
                 let result = await this.sp.web.getFolderByServerRelativePath("Shared Documents").files.addUsingPath(FolderPath, fileContent.type, { Overwrite: true });
            } else {
                // large upload
                let result = await this.sp.web.getFolderByServerRelativePath("Shared Documents").files.addChunked(FolderPath, fileContent.type, data => {
                    console.log(`progress`);
                }, true);
            }
        }
        catch (err) {
            // (error handling removed for simplicity)
            return Promise.resolve(false);
        }
    }
{
                                            id: "filePicker",
                                            title: "Select File",
                                            type: CustomCollectionFieldType.custom,
                                            onCustomRender: (field, value, onUpdate, item, itemId, onError) => {
                                                return (
                                                    React.createElement(FilePicker, {
                                                        key: itemId,
                                                        context: this.context,
                                                        buttonLabel: "Select File",
                                                        onChange: (filePickerResult: IFilePickerResult[]) => {
                                                            console.log('changing....', field);
                                                            onUpdate(field.id, filePickerResult[0]);
                                                            this.context.propertyPane.refresh();
                                                            this.render();
                                                        },
                                                        onSave:
                                                            async (filesPicked: IFilePickerResult[]) => {
                                                                for (const filePicked of filesPicked) {
                                                                    const filePickedContent = await filePicked.downloadFileContent();
                                                                    const folderPath = await filePicked.fileAbsoluteUrl
                                                                    this.uploadFiles(filePickedContent, filePicked.fileName, folderPath);
                                                                }
                                                            }
                                                    })
                                                );
                                            },
                                            required: true
                                        },

Yet when I upload an image, nothing happens. Is this being used the correct way?

joelfmrodrigues commented 2 years ago

@walleford sorry for the late reply, hope you had resolved the issue by now, but if not.. I can't really spot anything strange just by looking at the code. Are able to debug it and see where it may be failing? Are you able to get the contents of the file with downloadFileContent()? Have you tried using a small and bigger file to test both cases inside the uploadFiles function? Are you sure PnPjs is correctly setup? Are you able to retrieve data from a list as an example?

walleford commented 2 years ago

Hey Joel, Thank you for taking time to help me out, I did not get it figured out yet. When I run the above code in my project this is the output from the browser console: image While it is not rendering into the webpart: image

Along with the above code I use this to initialize sp: private sp = spfi().using(SPFx(this.context));

While this is my .tsx code for rendering:

 public render(): React.ReactElement<ICallingCardsProps> {

        let arr = this.props.CallingCards || [];
        console.log(this.props.CallingCards);

        function handleClick(el): any {
            window.open('mailto:' + el)
        }

        if (this.props.CallingCards && this.props.CallingCards.length > 0) {
            var contactsPicture = arr.map(el =>
                <div className={`${styles.tile}`}>            
                    <img key={el} src={el.filePicker.fileAbsoluteUrl} />
                    <div className={`${styles.textContainer}`}>
                        <div className={`${styles.nameStyles}`} key={el}>{el.Name}</div>
                        <div className={`${styles.textStyles}`} key={el}>{el.Position}</div>
                        <div className={`${styles.textStyles}`} key={el}>Phone: {el.PhoneNumber}</div>
                        <div className={`${styles.textStyles}`} key={el}>Email: </div>
                        <a className={`${styles.emailStyles}`} href={`mailto:${el.Email}`} target="_top" key={el}>{el.Email}</a>
                    </div>
                </div>
            )
        } else {
            return (
                <div className={`${styles.welcome}`}>Use property pane to create new Contact Cards!</div>
            )
        }

        return (
            <body>
                <div className={`${styles.grid}`}>
                    {contactsPicture}
                </div>
            </body>
        );
    }

I am at a loss as to what could be causing this issue because I have probably read every "pnpfilepicker spfx" blog/post/article I can find, yet still don't have a working solution.

walleford commented 2 years ago

@joelfmrodrigues I wanted to include this: image It does not seem like downloadFileContent is pulling the absolute URL for the image, which is what I am using to render it