Approve SharePoint Tasks in Batch with PnP JS

Approve SharePoint Tasks in Batch with PnP JS

Summary

If you have purely Yes/No tasks in order a Workflow to run then continue what it’s suppose to do, this is maybe something You’ll search.

Don’t forget PnP JS save you large amount of lines of code, and is my recommend approach for Batching Calls. It Must run in the context of the Webpart, so you have to give him context. This code belongs to a Webpart with a ListView Control from the React PnP Controls and PnP Reusable property pane controls for the SharePoint.

I didn’t create any Data Model Interface, that’s why I’m using Object.Keys and JSON.stringify from the REACT State. Loading property is for the UI Fabric Spinner.

Installation

I’m using SPFx version 1.8.2

PnP JS

https://pnp.github.io/pnpjs/

npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/graph --save

PnP React Controls

https://sharepoint.github.io/sp-dev-fx-controls-react/

npm install @pnp/spfx-controls-react --save --save-exact

PnP Property Controls

https://sharepoint.github.io/sp-dev-fx-property-controls/

npm install @pnp/spfx-property-controls --save --save-exact

The Web Part

Properties and State

import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from "@microsoft/sp-core-library";

export interface IKmTarefasLoteProps {
  context: WebPartContext;
  displayMode: DisplayMode;
  description: string;
  numberValue: number;
  list: string | string[];
  title: string;
  updateTitle: (value: string) => void;
  toggleEdicao: boolean;
}

export interface IKmTarefasLoteState {
  items?: any[];
  loading?: boolean;
  showPlaceholder?: boolean;
}
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';

import * as strings from 'KmTarefasLoteWebPartStrings';
import KmTarefasLote from './components/KmTarefasLote';
import { IKmTarefasLoteProps } from './components/IKmTarefasLoteProps';

import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
import { CalloutTriggers } from '@pnp/spfx-property-controls/lib/PropertyFieldHeader';
import { PropertyFieldSliderWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldSliderWithCallout';
import { PropertyFieldPeoplePicker, PrincipalType, IPropertyFieldGroupOrPerson } from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
import { PropertyFieldToggleWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldToggleWithCallout';

export interface IKmTarefasLoteWebPartProps {
  title: string;
  description: string;
  lists: string | string[];
  numberValue: number;
  people: IPropertyFieldGroupOrPerson[];
  toggleUtilizador: boolean;
  toggleEdicao: boolean;
}

export default class KmTarefasLoteWebPart extends BaseClientSideWebPart<IKmTarefasLoteWebPartProps> {

  public render(): void {
    const element: React.ReactElement<IKmTarefasLoteProps > = React.createElement(
      KmTarefasLote,
      {
        context: this.context,
        displayMode: this.displayMode,
        title: this.properties.title,
        updateTitle: (value: string) => {
          this.properties.title = value;
        },
        description: this.properties.description,
        numberValue: this.properties.numberValue || 50,
        list: this.properties.lists || "",
        toggleEdicao: this.properties.toggleEdicao
      }
    );

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

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

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

  protected get disableReactivePropertyChanges(): boolean {
    return true;
  }

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

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    let allGroups: any;

    if(!this.properties.toggleUtilizador)
    {
      allGroups = PropertyFieldPeoplePicker('people', {
        label: 'Grupos, Pessoas',
        initialData: this.properties.people,
        allowDuplicate: false,
        principalType: [PrincipalType.SharePoint, PrincipalType.Security, PrincipalType.Users],
        onPropertyChange: this.onPropertyPaneFieldChanged,
        context: this.context,
        properties: this.properties,
        onGetErrorMessage: null,
        deferredValidationTime: 0,
        key: 'peopleFieldId',
        disabled: false
      });
    } else {
      allGroups = PropertyFieldPeoplePicker('people', {
        label: 'Grupos, Pessoas',
        initialData: this.properties.people,
        allowDuplicate: false,
        principalType: [PrincipalType.SharePoint, PrincipalType.Security, PrincipalType.Users],
        onPropertyChange: this.onPropertyPaneFieldChanged,
        context: this.context,
        properties: this.properties,
        onGetErrorMessage: null,
        deferredValidationTime: 0,
        key: 'peopleFieldId',
        disabled: true
      });
    }
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: strings.DescriptionFieldLabel
                }),
                PropertyFieldListPicker('lists', {
                  label: 'Selecione uma Lista',
                  selectedList: this.properties.lists,
                  includeHidden: false,
                  orderBy: PropertyFieldListPickerOrderBy.Title,
                  disabled: false,
                  baseTemplate: 171,
                  onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
                  properties: this.properties,
                  context: this.context,
                  onGetErrorMessage: null,
                  deferredValidationTime: 0,
                  key: 'listPickerFieldId'
                }),
                PropertyFieldSliderWithCallout('sliderWithCalloutValue', {
                  calloutContent: React.createElement('div', {}, 'Selecione o número de items a listas'),
                  calloutTrigger: CalloutTriggers.Click,
                  calloutWidth: 200,
                  key: 'sliderWithCalloutFieldId',
                  label: 'Número de Items a listar',
                  max: 5000,
                  min: 50,
                  step: 50,
                  showValue: true,
                  value: this.properties.numberValue
                }),
                PropertyFieldToggleWithCallout('toggleUtilizador', {
                  calloutTrigger: CalloutTriggers.Click,
                  key: 'toggleInfoHeaderFieldId',
                  label: 'Permissões Únicas',
                  calloutContent: React.createElement('p', {}, this.properties.description),
                  onText: 'Utilizador com sessão iniciada',
                  offText: 'Utilizadores, Grupos',
                  checked: this.properties.toggleUtilizador
                }),
                allGroups,
                PropertyFieldToggleWithCallout('toggleEdicao', {
                  calloutTrigger: CalloutTriggers.Click,
                  key: 'toggleInfoHeaderFieldIdV',
                  label: 'Modo',
                  calloutContent: React.createElement('p', {}, this.properties.description),
                  onText: 'Edição',
                  offText: 'Visualização',
                  checked: this.properties.toggleEdicao
                }),
              ]
            }
          ]
        }
      ]
    };
  }
}

Putting all together with PnP Property Controls

REACT Component

The Properties

 import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from "@microsoft/sp-core-library";

export interface IKmTarefasLoteProps {
  context: WebPartContext;
  displayMode: DisplayMode;
  description: string;
  numberValue: number;
  list: string | string[];
  title: string;
  updateTitle: (value: string) => void;
  toggleEdicao: boolean;
}

export interface IKmTarefasLoteState {
  items?: any[];
  loading?: boolean;
  showPlaceholder?: boolean;
}

SCSS File

@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.kmTarefasLote {
  .container {
    max-width: 700px;
    margin: 0px auto;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  }

  .row {
    @include ms-Grid-row;
    @include ms-fontColor-white;
    background-color: $ms-color-themeDark;
    padding: 20px;
  }

  .column {
    @include ms-Grid-col;
    @include ms-lg10;
    @include ms-xl8;
    @include ms-xlPush2;
    @include ms-lgPush1;
  }

  .title {
    @include ms-font-xl;
    //@include ms-fontColor-white;
  }

  .subTitle {
    @include ms-font-l;
    //@include ms-fontColor-white;
  }

  .description {
    @include ms-font-l;
    //@include ms-fontColor-white;
  }

  .button {
    // Our button
    text-decoration: none;
    height: 32px;

    // Primary Button
    min-width: 80px;
    background-color: $ms-color-themePrimary;
    border-color: $ms-color-themePrimary;
    color: $ms-color-white;

    // Basic Button
    outline: transparent;
    position: relative;
    font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
    -webkit-font-smoothing: antialiased;
    font-size: $ms-font-size-m;
    font-weight: $ms-font-weight-regular;
    border-width: 0;
    text-align: center;
    cursor: pointer;
    display: inline-block;
    padding: 0 16px;

    .label {
      font-weight: $ms-font-weight-semibold;
      font-size: $ms-font-size-m;
      height: 32px;
      line-height: 32px;
      margin: 0 4px;
      vertical-align: top;
      display: inline-block;
    }
  }
}

TSX File

I´m rendering the behaviour of some columns to Match my needs

import * as React from 'react';
import styles from './KmTarefasLote.module.scss';
import { IKmTarefasLoteProps, IKmTarefasLoteState } from './IKmTarefasLoteProps';

import * as moment from 'moment';
import * as numeral from 'numeral';

import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/components/Spinner';
import { Placeholder } from '@pnp/spfx-controls-react/lib/Placeholder';

import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from '@pnp/spfx-controls-react/lib/controls/listView';
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { sp } from "@pnp/sp";

import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { initializeIcons } from '@uifabric/icons';
import { Icon, IconType } from 'office-ui-fabric-react/lib/Icon';

const selected = [];
var itemId = '';
export default class KmTarefasLote extends React.Component<IKmTarefasLoteProps, IKmTarefasLoteState> {

  //#region ListView Group Fields

  public groupByFields: IGrouping[] = [
    {
      name: "DueDate",
      order: GroupOrder.ascending
    }
  ];

  //#endregion

  //#region Super Props and Component

  /**
   * Constructor
   * @param props
   */
  constructor(props: IKmTarefasLoteProps) {
    super(props);

    this.state = {
      items: [],
      loading: false,
      showPlaceholder: (this.props.list === null || this.props.list === "")
    };
  }

  /**
   * componentDidMount lifecycle hook
   */
  public componentDidMount() {
    if (this.props.list !== null && this.props.list !== "" && this.props.list !== undefined) {
      this._getListItems();
    }
  }

  public componentDidUpdate(prevProps: IKmTarefasLoteProps, prevState: IKmTarefasLoteState) {
    if (this.props.list !== prevProps.list) {
      if (this.props.list !== null && this.props.list !== "" && this.props.list !== undefined) {
        this._getListItems();
      }
    }
  }

  //#endregion

  //#region GET via PNP SP JS

  private _getListItems() {
    this.setState({
      loading: true
    });

    if (Environment.type == EnvironmentType.SharePoint ||
      Environment.type == EnvironmentType.ClassicSharePoint) {
      sp.web.lists.getById(`${this.props.list.toString()}`)
        .items.filter(`substringof('${encodeURIComponent("Leitura")}',Title) and PercentComplete+lt+1`)
        .select("Id,Title,Body,PercentComplete,AssignedTo/Title,AssignedTo/ID,Created").expand("AssignedTo")
        .top(parseInt(this.props.numberValue.toString()))
        .getAll().then((allItems: any[]) => {
          console.log(allItems);
          this.setState({
            items: allItems.length ? allItems : [],
            loading: false,
            showPlaceholder: false
          });
          // how many did we get
          console.log(allItems.length);
        });
    }
    else {
      this.setState({
        items: [
          {
            Id: '10',
            Title: 'Leitura Correspondência - C_4',
            AssignedTo: 'Administrativas',
            _Comments: 'Comentários',
            PercentComplete: 0,
            Created: '2018-03-23T10:09:09Z'
          },
          {
            Id: '20',
            Title: 'Leitura Correspondência - C_6',
            AssignedTo: '2019-03-19T10:09:09Z',
            _Comments: 'Comentários',
            PercentComplete: 0,
            Created: '2018-03-23T10:09:09Z'
          },
          {
            Id: '30',
            Title: 'Leitura Correspondência - C_8',
            AssignedTo: '2019-03-19T10:09:09Z',
            _Comments: 'Comentários',
            PercentComplete: 1,
            Created: '2018-03-23T10:09:09Z'
          },
        ],
        loading: false,
        showPlaceholder: false
      });
    }
  }

  //#endregion

  //#region WP

  private _configureWebPart() {
    this.props.context.propertyPane.open();
  }

  //#endregion

  //#region Bulk Approve POST

  // Approve all tasks

  private _appproveTasks() {
    if (Environment.type == EnvironmentType.SharePoint ||
      Environment.type == EnvironmentType.ClassicSharePoint) {
      this.setState({
        loading: true
      });
      sp.setup({
        spfxContext: this.props.context
      });

      let list = sp.web.lists.getById(this.props.list.toString());
      list.getListItemEntityTypeFullName().then(entityTypeFullName => {
        let batch = sp.web.createBatch();
        try {
          Object.keys(this.state.items).map(i =>
            list.items.getById(this.state.items[i]['ID']).inBatch(batch).update({ PercentComplete: JSON.stringify(this.state.items[i]['ID']) }, "*",
              entityTypeFullName).then(b => {
                console.log(b);
              }));
        } catch (error) {
          this.setState({
            loading: false
          });
        }
        batch.execute().then(d => console.log("Done")).then(l => this._getListItems());
      });
      this.setState({
        loading: false
      });
      this.forceUpdate();
    }
  }

  private handleClick(source: string, event) {
    alert(`${source} clicked`);
  }

  //#endregion

  //#region ViewFields

  private _viewFields: IViewField[] = [
    // {
    //   name: "ID",
    //   displayName: "ID",

    //   maxWidth: 1,
    //   minWidth: 1,
    //   sorting: true,
    //   render: (item: any) => {
    //     const created = item["ID"];
    //     if (created) {
    //       itemId = JSON.stringify(created);
    //       return <span>{created}</span>;
    //     }
    //   }
    // },
    {
      name: "Title",
      linkPropertyName: "File.ServerRelativeUrl",
      displayName: "Tarefa",
      isResizable: true,
      sorting: true,
      minWidth: 100,
      maxWidth: 250,
      render: (item: any) => {
        const itemTitle = item["Title"];
        var createUrl = '';
        console.log(itemId + '  ' + itemTitle);
        if (itemTitle) {
          if (this.props.toggleEdicao) {
            createUrl = this.props.context.pageContext.site.absoluteUrl + '/_layouts/listform.aspx?PageType=4&ListId=' + this.props.list.toString() + '&ID=' + item["ID"];
          } else {
            createUrl = this.props.context.pageContext.site.absoluteUrl + '/_layouts/listform.aspx?PageType=6&ListId=' + this.props.list.toString() + '&ID=' + item["ID"];
          }
          console.log(createUrl);
          return <a href={createUrl}>{JSON.stringify(itemTitle).substr(1).slice(0, -1)}</a>;
        }
      }
    },
    {
      name: "AssignedTo.0.Title",
      displayName: "Atribuído a",
      sorting: true,
      minWidth: 100,
      // maxWidth: 100,
      render: (item: any) => {
        const created = item["AssignedTo.0.Title"];
        const createUrl = this.props.context.pageContext.site.absoluteUrl + '/_layouts/15/userdisp.aspx?ID=' + JSON.stringify(item['AssignedTo.0.ID']);
        if (created) {
          return <a href={createUrl}>{created}</a>;
        }
      }
    },
    {
      name: "Created",
      displayName: "Criação",
      sorting: true,
      minWidth: 100,
      maxWidth: 100,
      render: (item: any) => {
        const created = item["Created"];
        if (created) {
          const createdDate = moment(created);
          return <span>{createdDate.format('YYYY-MM-DD')}</span>;
        }
      }
    },
    {
      name: "PercentComplete",
      displayName: "A",
      sorting: false,
      maxWidth: 20,
      minWidth: 20,
      render: (item: any) => {
        const created = item["PercentComplete"];
        console.log(JSON.stringify(created));
        if (JSON.stringify(created) === '0') {
          //console.log(JSON.stringify(created));
          return <span><Icon iconName="Rotate" /></span>;
        }
        else {
          return <span><Icon iconName="SkypeCheck" /></span>;
        }
        // }
      }
    }
  ];

  //#endregion

  //#region Render

  private _getSelection(items: any[]) {
    if (items.length !== 0) {
      for (let i = 0; i < items.length; i++) {
        selected.push(items[i]['Id']);
      }
      console.log(JSON.stringify(selected));
      selected.length = 0;
    }
  }

  public render(): React.ReactElement<IKmTarefasLoteProps> {
    // Check if placeholder needs to be shown
    if (this.state.showPlaceholder) {
      return (
        <Placeholder
          iconName="DocumentManagement"
          iconText="CONFIGURAÇÕES INICIAIS"
          description="Configure as opções mandatórias, clique em Configurar."
          buttonLabel="Configurar"
          onConfigure={this._configureWebPart.bind(this)} />
      );
    }
    return (
      <div>
        {
          this.state.loading ?
            (
              <Spinner size={SpinnerSize.large} label="A obter resultados ..." />
            ) : (
              this.state.items.length === 0 ?
                (
                  <Placeholder
                    iconName="Like"
                    iconText="Todas as tarefas de Leitura Aprovadas"
                    description="A serem criadas Tarefas poderá aprova-las em Lote!" />
                ) : (
                  <div className={styles.kmTarefasLote}>
                    <span className={styles.title}>{this.props.description}</span>
                    {/* <div className={styles.column}> */}
                    <WebPartTitle className={styles.subTitle} displayMode={this.props.displayMode}
                      title={this.props.title}
                      updateProperty={this.props.updateTitle} />
                    {/* <div className={styles.container}> */}
                    {/* <div className={styles.row}> */}
                    {/* <a href="#" className={styles.button} onClick={() => { if (window.confirm('Quer mesmo aprovar todas os processos?')) this._appproveTasks(); }} >
                        <span className={styles.label}>Aprovar Processos</span>
                      </a> */}
                    <PrimaryButton onClick={() => { if (window.confirm('Quer mesmo aprovar todas os processos?')) this._appproveTasks(); }} text="Aprovar todas" />
                    {/* <DefaultButton disabled={!selected.length} onClick={() => { if (window.confirm('Quer mesmo aprovar todas os processos?')) this._appproveTasks(); }} text="Aprovar Selecionado" /> */}
                    <br></br><br></br>
                    <ListView
                      items={this.state.items}
                      viewFields={this._viewFields}
                      compact={true}
                      selectionMode={SelectionMode.none}
                      selection={this._getSelection}
                      filterPlaceHolder={"Pesquisar..."}
                      showFilter={true}
                      iconFieldName="File.ServerRelativeUrl" />
                    {/* groupByFields={this.groupByFields} /> */}
                    {/* </div> */}
                    {/* </div> */}
                    {/* </div> */}
                  </div>
                )
            )
        }
      </div>
    );
  }

  //#endregion
}

JSON Part MANIFEST

{
  "name": "km-webparts",
  "version": "0.0.1",
  "private": true,
  "engines": {
    "node": ">=0.10.0"
  },
  "scripts": {
    "build": "gulp bundle",
    "clean": "gulp clean",
    "test": "gulp test"
  },
  "dependencies": {
    "@microsoft/sp-core-library": "1.8.2",
    "@microsoft/sp-lodash-subset": "1.8.2",
    "@microsoft/sp-office-ui-fabric-core": "1.8.2",
    "@microsoft/sp-property-pane": "1.8.2",
    "@microsoft/sp-webpart-base": "1.8.2",
    "@pnp/common": "^1.3.2",
    "@pnp/graph": "^1.3.2",
    "@pnp/logging": "^1.3.2",
    "@pnp/odata": "^1.3.2",
    "@pnp/sp": "^1.3.2",
    "@pnp/spfx-controls-react": "1.13.0",
    "@pnp/spfx-property-controls": "1.14.1",
    "@types/es6-promise": "0.0.33",
    "@types/react": "16.7.22",
    "@types/react-dom": "16.8.0",
    "@types/webpack-env": "1.13.1",
    "numeral": "^2.0.6",
    "office-ui-fabric-react": "6.143.0",
    "react": "16.7.0",
    "react-dom": "16.7.0"
  },
  "resolutions": {
    "@types/react": "16.7.22"
  },
  "devDependencies": {
    "@microsoft/sp-build-web": "1.8.2",
    "@microsoft/sp-tslint-rules": "1.8.2",
    "@microsoft/sp-module-interfaces": "1.8.2",
    "@microsoft/sp-webpart-workbench": "1.8.2",
    "@microsoft/rush-stack-compiler-2.9": "0.7.7",
    "gulp": "~3.9.1",
    "@types/chai": "3.4.34",
    "@types/mocha": "2.2.38",
    "ajv": "~5.2.2"
  }
}

TS Config

Typescript based Rush 2.9

{
  "extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
  "compilerOptions": {
    "target": "es5",
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "react",
    "declaration": true,
    "sourceMap": true,
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "outDir": "lib",
    "inlineSources": false,
    "strictNullChecks": false,
    "noUnusedLocals": false,
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@microsoft"
    ],
    "types": [
      "es6-promise",
      "webpack-env"
    ],
    "lib": [
      "es5",
      "dom",
      "es2015.collection"
    ]
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "lib"
  ]
}

Leave a Reply

Your email address will not be published. Required fields are marked *

RSS