pnp / pnpjs

Fluent JavaScript API for SharePoint and Microsoft Graph REST APIs
https://pnp.github.io/pnpjs/
Other
742 stars 302 forks source link

@pnp/graph - spfx msal v3 single sign on redirect #3018

Closed pmmarkley closed 2 months ago

pmmarkley commented 2 months ago

Major Version

4.x

Minor Version Number

0.1

Target environment

SharePoint Framework

Additional environment details

Node v18.20.2 Sharepoint Framework 1.18.2 @pnp/cli-microsoft365@7.6.0 corepack@0.25.2 gulp@4.0.2 npm@10.5.0 yo@5.0.0

Expected or Desired Behavior

I have followed Getting Started with PnpJs

I expect that

const _graph = getGraph(this.props.context);
const events = await _graph.me.calendar.events(); 
console.log(events.length);

returns the number of events in my calendar.

Observed Behavior

The call to getGraph returns fine, however when attempting to retrieve the events from my calendar the page reloads, it looks like it is redirecting to msal v3 single sign on redirect before the page reloads.

Steps to Reproduce

Here is the code I'm running:

pnpjsConfig.ts

import { WebPartContext } from "@microsoft/sp-webpart-base";
import { spfi, SPFI,SPFx as spSPFx } from "@pnp/sp";
import { graphfi, GraphFI,SPFx as graphSPFx } from "@pnp/graph";
import { LogLevel, PnPLogging } from "@pnp/logging";

import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";
import "@pnp/graph/calendars";
import "@pnp/graph/users";

//@ts-expect-error this is due to esLint being overactive
let _sp: SPFI = null;
//@ts-expect-error this is due to esLint being overactive
let _graph:GraphFI = null;

export const getSP = (context?: WebPartContext): SPFI => {

    if (context !== null && _sp === null) {
        //@ts-expect-error blerb
        _sp = spfi().using(spSPFx(context)).using(PnPLogging(LogLevel.Warning));
    }
    return _sp;
}

export const getGraph = (context?: WebPartContext): GraphFI => {

    if (context !== null && _graph === null) {
        //@ts-expect-error blerb
        _graph = graphfi().using(graphSPFx(context)).using(PnPLogging(LogLevel.Warning));
    }
    return _graph;
}

CcgTimeRequestVisualizer.tsx

import * as React from 'react';
import styles from './CcgTimeRequestVisualizer.module.scss';
import type { ICcgTimeRequestVisualizerProps } from './ICcgTimeRequestVisualizerProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { ICcgTimeRequestVisualizerState } from './ICcgTimeRequestVisualizerState';
import { getGraph } from '../pnpjsConfig';
import '@pnp/graph';
import { IUser } from '@pnp/graph/users';

export default class CcgTimeRequestVisualizer extends React.Component<ICcgTimeRequestVisualizerProps, ICcgTimeRequestVisualizerState> {

  constructor(props: ICcgTimeRequestVisualizerProps) {

    super(props);
  }

  private async _addToCalendar(): Promise<void> {
    const _graph = getGraph(this.props.context);
    const events = await _graph.me.calendar.events();
    console.log(events.length);

await _graph.me.calendar.events.add({
      "subject": "Katie Alley - Perk",
      "body": {
        "content": "Katie Alley - Perk"
      },
      "start": {
          "dateTime": "2024-05-10T08:00:00",
          "timeZone": "Eastern Standard Time"
      },
      "end": {
          "dateTime": "2024-05-10T17:00:00",
          "timeZone": "Eastern Standard Time"
      }
    });
  }
  public render(): React.ReactElement<ICcgTimeRequestVisualizerProps> {
    const {
      listName,
      userDisplayName
    } = this.props;

    return (
      <section className={`${styles.ccgTimeRequestVisualizer}`}>
        <div className={styles.welcome}>
          <h2>Well done, {escape(userDisplayName)}!</h2>
          <p>You have selected ${listName}</p>
        </div>
        <button type="button" onClick={ this._addToCalendar.bind(this)}>Add To Calendar</button>
      </section>
    );
  }
}
juliemturner commented 2 months ago

So, there are a couple of things, one I can see right away and the other I'm not sure because you didn't share enough code. But essentially, you're call in ``

private async _addToCalendar(): Promise<void> {
    const _graph = getGraph(this.props.context);
    const events = await _graph.me.calendar.events();
    console.log(events.length);
    ....

should not be passing this.props.context. The point of the pnpjsConfig.ts file implementation is that you initialize the variable when you're webpart first loads and then when you need an instance of the graphfi interface you can make a call (without context) to return it. Since it looks like you copied this from our sample, I would take a look at the onInit call in the root webpart ts file so you can see what I'm suggesting.

pmmarkley commented 2 months ago

Thank you for your response, I have removed the this.props.context from the call to getGraph and the behavior is still the same. Below is the code in my CcgTimeRequestVisualizerWebPart.ts. In this file the _getLists function works as expected. This issue only seems related to using graph.

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  type IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneDropdown,
  IPropertyPaneDropdownOption,
  PropertyPaneCheckbox
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';

import * as strings from 'CcgTimeRequestVisualizerWebPartStrings';
import { ICcgTimeRequestVisualizerProps } from './components/ICcgTimeRequestVisualizerProps';
import { getSP, getGraph } from './pnpjsConfig';
import { LogLevel, Logger } from '@pnp/logging';
import { IList, IListInfo } from '@pnp/sp/lists';
import { SPFI } from '@pnp/sp';
import CcgTimeRequestVisualizer from './components/CcgTimeRequestVisualizer';
import { GraphFI } from '@pnp/graph';

export interface ICcgTimeRequestVisualizerWebPartProps {
  description: string;
  listName: string;
  newFormPageName: string;
  editFormPageName: string;
  privacyEnabled: boolean;
}

export default class CcgTimeRequestVisualizerWebPart extends BaseClientSideWebPart<ICcgTimeRequestVisualizerWebPartProps> {

  private _isDarkTheme: boolean = false;
  private _lists: IPropertyPaneDropdownOption[];
  private _pages: IPropertyPaneDropdownOption[];
  private _listDropdownDisabled: boolean = true;
  private _pageDropdownDisabled: boolean = true;

  public render(): void {
    const element =  React.createElement<ICcgTimeRequestVisualizerProps>(
      CcgTimeRequestVisualizer,
      {
        description: this.properties.description,
        isDarkTheme: this._isDarkTheme,
        hasTeamsContext: !!this.context.sdks.microsoftTeams,
        userDisplayName: this.context.pageContext.user.displayName,
        listName: this.properties.listName,
        context: this.context
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected async onInit(): Promise<void> {
    await super.onInit();

    // Initialize our _sp and _graph objects so we can then use iin other packages without having to pass around the context.
    getSP(this.context);
    getGraph(this.context);

  }

  // method to pull all the lists in the current site.
  private _getLists = async (): Promise<void> => {
    this._lists = [];
    try {
      const _sp = getSP();
      const response:IListInfo[] = await _sp.web.lists();
      response.forEach((list: IListInfo) => {
        if (list.BaseTemplate === 100) {
          this._lists.push({
            key: list.Title,
            text: list.Title
          });
        }
      });
    } catch (err) {
      Logger.write(`_getLists - ${JSON.stringify(err)} - `, LogLevel.Error);
    }
  }

  protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
    if (!currentTheme) {
      return;
    }

    this._isDarkTheme = !!currentTheme.isInverted;
    const {
      semanticColors
    } = currentTheme;

    if (semanticColors) {
      this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
      this.domElement.style.setProperty('--link', semanticColors.link || null);
      this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
    }

  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected onPropertyPaneConfigurationStart(): void {
    // disable drop down if there are no items
    this._listDropdownDisabled= !this._lists;
    // if the lists dropdown has items, then return
    if(this._lists){
      return;
    }
    this.context.statusRenderer.displayLoadingIndicator(this.domElement,'lists');
    this._getLists().then(() => {
      this._listDropdownDisabled = false;
      this.context.propertyPane.refresh();
      this.context.statusRenderer.clearLoadingIndicator(this.domElement);
    });

  }

  // triggered when there is a change to a property pane item
  // compare the old value with the newly selected value and see if any updates are required.
  protected onPropertyPaneFieldChanged(propertyPath:string, oldValue:any, newValue: any){
    super.onPropertyPaneFieldChanged(propertyPath,oldValue,newValue);
    this.render();
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: strings.DescriptionFieldLabel
                }),
                PropertyPaneDropdown('listName', {
                  label: strings.ListNameFieldLabel,
                  options: this._lists,
                  disabled: this._listDropdownDisabled
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

And the updated code from my CcgTimeRequestVisualizer.tsx file (removing the this.props.context from the call to getGraph

import * as React from 'react';
import styles from './CcgTimeRequestVisualizer.module.scss';
import type { ICcgTimeRequestVisualizerProps } from './ICcgTimeRequestVisualizerProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { ICcgTimeRequestVisualizerState } from './ICcgTimeRequestVisualizerState';
import { getGraph } from '../pnpjsConfig';
import '@pnp/graph';
import { IUser } from '@pnp/graph/users';

export default class CcgTimeRequestVisualizer extends React.Component<ICcgTimeRequestVisualizerProps, ICcgTimeRequestVisualizerState> {

  constructor(props: ICcgTimeRequestVisualizerProps) {

    super(props);
  }

  private async _addToCalendar(): Promise<void> {
    const _graph = getGraph();
    const events = await _graph.me.calendar.events();
    console.log(events.length);
    await _graph.me.calendar.events.add({
      "subject": "Katie Alley - Perk",
      "body": {
        "content": "Katie Alley - Perk"
      },
      "start": {
          "dateTime": "2024-05-10T08:00:00",
          "timeZone": "Eastern Standard Time"
      },
      "end": {
          "dateTime": "2024-05-10T17:00:00",
          "timeZone": "Eastern Standard Time"
      }
    });
  }
  public render(): React.ReactElement<ICcgTimeRequestVisualizerProps> {
    const {
      listName,
      userDisplayName
    } = this.props;

    return (
      <section className={`${styles.ccgTimeRequestVisualizer}`}>
        <div className={styles.welcome}>
          <h2>Well done, {escape(userDisplayName)}!</h2>
          <p>You have selected ${listName}</p>
        </div>
        <button type="button" onClick={ this._addToCalendar.bind(this)}>Add To Calendar</button>
      </section>
    );
  }
}
bcameron1231 commented 2 months ago

I'm having trouble reproducing. I don't have any issues with reloading and it's working as expected. Can you provide a bit more context of your environment? What's your browser, are you loading just in SharePoint, Workbench, Teams? Have you tried different browsers?

Looking through some recent issues in the SP-Dev-Docs site, I wonder if it's an issue outside of the library, and a problem on Microsoft's side.

https://github.com/SharePoint/sp-dev-docs/issues/9485

pmmarkley commented 2 months ago

My apologizes, to fix this issue I simply needed to login to SharePoint admin center and grant access for Microsoft Graph to Calendars.ReadWrite. Thanks for your help.

github-actions[bot] commented 2 months ago

This issue is locked for inactivity or age. If you have a related issue please open a new issue and reference this one. Closed issues are not tracked.