Partager via


Migrer jQuery et la solution FullCalendar créée à l’aide du composant WebPart Éditeur de script vers SharePoint Framework

Lorsque vous créez des solutions SharePoint, les développeurs SharePoint utilisent souvent le plug-in jQuery FullCalendar pour afficher des données dans l’affichage Calendrier. FullCalendar est une alternative intéressante à l’affichage Calendrier standard de SharePoint, car il permet d’afficher sous forme de calendrier des données provenant de plusieurs listes de calendriers, des données provenant de listes autres que des listes de calendriers ou même des données se trouvant en dehors de SharePoint. Cet article explique comment migrer une personnalisation SharePoint à l’aide de FullCalendar créé avec le composant WebPart Éditeur de script vers SharePoint Framework.

Liste des tâches affichée sous la forme d’un calendrier créé à l’aide du composant WebPart Éditeur de script

Pour illustrer le processus de migration d’une personnalisation SharePoint à l’aide de FullCalendar vers SharePoint Framework, vous allez utiliser la solution suivante qui permet d’obtenir un affichage Calendrier des tâches récupérées à partir d’une liste SharePoint.

Affichage Calendrier des tâches affichées sur une page SharePoint

La solution est générée à l’aide du composant WebPart Éditeur de script SharePoint standard. Voici le code utilisé par la personnalisation.

<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js"></script>
<link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
<div id="calendar"></div>

<script>
  var PATH_TO_DISPFORM = _spPageContextInfo.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx";
  var TASK_LIST = "Tasks";
  var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];

  displayTasks();

  function displayTasks() {
    $('#calendar').fullCalendar('destroy');
    $('#calendar').fullCalendar({
      weekends: false,
      header: {
        left: 'prev,next today',
        center: 'title',
        right: 'month,basicWeek,basicDay'
      },
      displayEventTime: false,
      // open up the display form when a user clicks on an event
      eventClick: function (calEvent, jsEvent, view) {
        window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id;
      },
      editable: true,
      timezone: "UTC",
      droppable: true, // this allows things to be dropped onto the calendar
      // update the end date when a user drags and drops an event
      eventDrop: function (event, delta, revertFunc) {
        updateTask(event.id, event.start, event.end);
      },
      // put the events on the calendar
      events: function (start, end, timezone, callback) {
        var startDate = start.format('YYYY-MM-DD');
        var endDate = end.format('YYYY-MM-DD');

        var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";

        $.ajax({
          url: _spPageContextInfo.webAbsoluteUrl + restQuery,
          type: "GET",
          dataType: "json",
          headers: {
            Accept: "application/json;odata=nometadata"
          }
        })
          .done(function (data, textStatus, jqXHR) {
            var personColors = {};
            var colorNo = 0;

            var events = data.value.map(function (task) {
              var assignedTo = task.AssignedTo.map(function (person) {
                return person.Title;
              }).join(', ');

              var color = personColors[assignedTo];
              if (!color) {
                color = COLORS[colorNo++];
                personColors[assignedTo] = color;
              }
              if (colorNo >= COLORS.length) {
                colorNo = 0;
              }

              return {
                title: task.Title + " - " + assignedTo,
                id: task.ID,
                color: color, // specify the background color and border color can also create a class and use className parameter.
                start: moment.utc(task.StartDate).add("1", "days"),
                end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
              };
            });

            callback(events);
          });
      }
    });
  }

  function updateTask(id, startDate, dueDate) {
    // subtract the previously added day to the date to store correct date
    var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
      startDate.format("hh:mm") + ":00Z";
    if (!dueDate) {
      dueDate = startDate;
    }
    var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
      dueDate.format("hh:mm") + ":00Z";

    $.ajax({
      url: _spPageContextInfo.webAbsoluteUrl + '/_api/contextinfo',
      type: 'POST',
      headers: {
        'Accept': 'application/json;odata=nometadata'
      }
    })
      .then(function (data, textStatus, jqXHR) {
        return $.ajax({
          url: _spPageContextInfo.webAbsoluteUrl +
          "/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")",
          type: 'POST',
          data: JSON.stringify({
            StartDate: sDate,
            DueDate: dDate,
          }),
          headers: {
            Accept: "application/json;odata=nometadata",
            "Content-Type": "application/json;odata=nometadata",
            "X-RequestDigest": data.FormDigestValue,
            "IF-MATCH": "*",
            "X-Http-Method": "PATCH"
          }
        });
      })
      .done(function (data, textStatus, jqXHR) {
        alert("Update Successful");
      })
      .fail(function (jqXHR, textStatus, errorThrown) {
        alert("Update Failed");
      })
      .always(function () {
        displayTasks();
      });
  }
</script>

Remarque

Cette solution repose sur le travail de Mark Rackley, MVP, Office Servers and Services, et directeur de la stratégie chez PAIT Group. Pour plus d’informations sur la solution d’origine, consultez l’article sur l’utilisation de FullCalendar.io pour créer des calendriers personnalisés dans SharePoint.

Tout d’abord, la personnalisation charge les bibliothèques qu’elle utilise : jQuery, Moment.js et FullCalendar aux premiers éléments <script> et <link>.

Ensuite, elle définit l’élément <div> dans lequel l’affichage Calendrier généré est injecté.

Puis, elle définit deux fonctions : displayTasks(), utilisée pour afficher les tâches dans l’affichage Calendrier, et updateTask(), qui est déclenchée suite au glisser-déplacer d’une tâche à une date différente et qui met à jour les dates sur l’élément de liste sous-jacent. Chaque fonction définit sa propre requête REST qui est ensuite utilisée pour communiquer avec l’API REST de la liste SharePoint pour récupérer ou mettre à jour des éléments de liste.

Grâce au plug-in jQuery FullCalendar, avec peu d’efforts, les utilisateurs obtiennent des solutions optimales pouvant utiliser des couleurs différentes pour marquer des événements différents ou l’option glisser-déplacer pour réorganiser des événements.

Glisser-déplacer des événements dans FullCalendar pour replanifier des tâches sous-jacentes

Migrer la solution Calendrier des tâches du Composant WebPart Éditeur de script vers SharePoint Framework

La migration d’une personnalisation basée sur un composant WebPart Éditeur de script vers SharePoint Framework offre de nombreux avantages, par exemple, une configuration plus conviviale et une gestion centralisée de la solution. Voici une description détaillée de la migration de la solution vers SharePoint Framework.

Tout d’abord, vous allez migrer la solution vers SharePoint Framework en apportant le moins de changements possible au code d’origine. Ensuite, vous allez transformer le code de la solution en TypeScript pour bénéficier de ses fonctionnalités de sécurité des types de temps de développement et remplacer une partie du code par l’API SharePoint Framework afin de tirer parti de ses fonctionnalités et simplifier davantage la solution.

Création d’un projet SharePoint Framework

  1. Commencez par créer un dossier pour votre projet :

    md fullcalendar-taskscalendar
    
  2. Accédez au dossier du projet :

    cd fullcalendar-taskscalendar
    
  3. Dans le dossier du projet, exécutez le générateur Yeoman pour SharePoint Framework afin de structurer un projet SharePoint Framework :

    yo @microsoft/sharepoint
    
  4. Lorsque vous y êtes invité, entrez les valeurs suivantes (sélectionnez l’option par défaut pour toutes les invites qui ne sont pas mentionnées ci-dessous) :

    • Quel est le nom de votre solution ? : fullcalendar-taskscalendar
    • Quels packages de base voulez-vous cibler pour votre ou vos composants ? : SharePoint Online uniquement (dernière version)
    • Quel type de composant côté client voulez-vous créer ? : WebPart
    • Quel est le nom de votre composant WebPart ? : calendrier de tâches
    • Quelle est la description de votre composant WebPart ? : affiche les tâches dans l’affichage Calendrier
    • Quelle infrastructure voulez-vous utiliser ? : Aucune infrastructure JavaScript
  5. Ensuite, ouvrez le dossier de votre projet dans votre éditeur de code. Dans ce didacticiel, vous allez utiliser Visual Studio Code.

Charger les bibliothèques JavaScript

Comme pour la solution d’origine créée à l’aide du composant WebPart Éditeur de script, vous devez d’abord charger les bibliothèques JavaScript requises par la solution. Dans SharePoint Framework, cela nécessite généralement deux étapes : spécifier l’URL de la bibliothèque à charger et faire référence à la bibliothèque dans le code.

  1. Spécifiez les URL des bibliothèques à charger.

    Dans l’éditeur de code, ouvrez le fichier ./config/config.json et modifiez la section externals comme suit :

    {
      // ..
      "externals": {
        "jquery": {
          "path": "https://code.jquery.com/jquery-1.11.1.min.js",
          "globalName": "jQuery"
        },
        "fullcalendar": {
          "path": "https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js",
          "globalName": "jQuery",
          "globalDependencies": [
            "jquery"
          ]
        },
        "moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"
      },
      // ..
    }
    
  2. Ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts et, après la dernière instruction import, ajoutez les codes suivants :

    import 'jquery';
    import 'moment';
    import 'fullcalendar';
    

Définir le div du conteneur

Comme dans la solution d’origine, l’étape suivante consiste à définir l’emplacement où le calendrier doit être affiché.

Dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts et modifiez la méthode render() comme suit :

export default class ItRequestsWebPart extends BaseClientSideWebPart<IItRequestsWebPartProps> {
  public render(): void {
    this.domElement.innerHTML = `
      <div class="${styles.tasksCalendar}">
        <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
        <div id="calendar"></div>
      </div>`;
  }
  // ...
}

Lancer FullCalendar et charger les données

La dernière étape consiste à inclure le code qui lance le plug-in jQuery FullCalendar et charge les données à partir de SharePoint.

  1. Dans le dossier ./src/webparts/tasksCalendar, créez un fichier nommé script.js et collez le code ci-dessous :

    var moment = require('moment');
    
    var PATH_TO_DISPFORM = window.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx";
    var TASK_LIST = "Tasks";
    var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
    
    displayTasks();
    
    function displayTasks() {
      $('#calendar').fullCalendar('destroy');
      $('#calendar').fullCalendar({
        weekends: false,
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,basicWeek,basicDay'
        },
        displayEventTime: false,
        // open up the display form when a user clicks on an event
        eventClick: function (calEvent, jsEvent, view) {
          window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id;
        },
        editable: true,
        timezone: "UTC",
        droppable: true, // this allows things to be dropped onto the calendar
        // update the end date when a user drags and drops an event
        eventDrop: function (event, delta, revertFunc) {
          updateTask(event.id, event.start, event.end);
        },
        // put the events on the calendar
        events: function (start, end, timezone, callback) {
          var startDate = start.format('YYYY-MM-DD');
          var endDate = end.format('YYYY-MM-DD');
    
          var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
              Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
              $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
    
          $.ajax({
            url: window.webAbsoluteUrl + restQuery,
            type: "GET",
            dataType: "json",
            headers: {
              Accept: "application/json;odata=nometadata"
            }
          })
            .done(function (data, textStatus, jqXHR) {
              var personColors = {};
              var colorNo = 0;
    
              var events = data.value.map(function (task) {
                var assignedTo = task.AssignedTo.map(function (person) {
                  return person.Title;
                }).join(', ');
    
                var color = personColors[assignedTo];
                if (!color) {
                  color = COLORS[colorNo++];
                  personColors[assignedTo] = color;
                }
                if (colorNo >= COLORS.length) {
                  colorNo = 0;
                }
    
                return {
                  title: task.Title + " - " + assignedTo,
                  id: task.ID,
                  color: color, // specify the background color and border color can also create a class and use className parameter.
                  start: moment.utc(task.StartDate).add("1", "days"),
                  end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
                };
              });
    
              callback(events);
            });
        }
      });
    }
    
    function updateTask(id, startDate, dueDate) {
      // subtract the previously added day to the date to store correct date
      var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        startDate.format("hh:mm") + ":00Z";
      if (!dueDate) {
        dueDate = startDate;
      }
      var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        dueDate.format("hh:mm") + ":00Z";
    
      $.ajax({
        url: window.webAbsoluteUrl + '/_api/contextinfo',
        type: 'POST',
        headers: {
          'Accept': 'application/json;odata=nometadata'
        }
      })
        .then(function (data, textStatus, jqXHR) {
          return $.ajax({
            url: window.webAbsoluteUrl +
            "/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")",
            type: 'POST',
            data: JSON.stringify({
              StartDate: sDate,
              DueDate: dDate,
            }),
            headers: {
              Accept: "application/json;odata=nometadata",
              "Content-Type": "application/json;odata=nometadata",
              "X-RequestDigest": data.FormDigestValue,
              "IF-MATCH": "*",
              "X-Http-Method": "PATCH"
            }
          });
        })
        .done(function (data, textStatus, jqXHR) {
          alert("Update Successful");
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
          alert("Update Failed");
        })
        .always(function () {
          displayTasks();
        });
    }
    

Ce code est presque identique au code d’origine de la personnalisation du composant WebPart Éditeur de script. La seule différence est qu’à l’endroit où le code d’origine a récupéré l’URL du site web actuel à partir de l’ensemble de variables général _spPageContextInfo défini par SharePoint, le code dans SharePoint Framework utilise une variable personnalisée que vous devrez définir dans le composant WebPart.

Les composants WebPart côté client de SharePoint Framework peuvent être utilisés à la fois sur les pages classiques et modernes. La variable _spPageContextInfo est présente sur les pages classiques mais elle n’est pas disponible sur les pages modernes, c’est pourquoi vous ne pouvez pas l’utiliser et avez besoin d’une propriété personnalisée que vous pouvez contrôler vous-même.

  1. Pour faire référence à ce fichier dans le composant WebPart, dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts et modifiez la méthode render() comme suit :

    export default class ItRequestsWebPart extends BaseClientSideWebPart<IItRequestsWebPartProps> {
      public render(): void {
        this.domElement.innerHTML = `
          <div class="${styles.tasksCalendar}">
            <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
            <div id="calendar"></div>
          </div>`;
    
        (window as any).webAbsoluteUrl = this.context.pageContext.web.absoluteUrl;
        require('./script');
      }
      // ...
    }
    
  2. Vérifiez que le composant WebPart fonctionne comme prévu en exécutant ce qui suit dans la ligne de commande :

    gulp serve --nobrowser
    

    Étant donné que le composant WebPart charge ses données à partir de SharePoint, vous devez tester le composant WebPart à l’aide de SharePoint Framework Workbench hébergé.

  3. Accédez à https://{nom-client-nom}. SharePoint. com/_layouts/Workbench.aspx et ajoutez le composant WebPart à la zone de dessin. Vous devez maintenant voir les tâches dans un affichage Calendrier à l’aide du plug-in jQuery FullCalendar.

    Tâches présentées dans un affichage Calendrier dans un composant WebPart SharePoint Framework côté client

Ajouter la prise en charge de la configuration du composant WebPart via les propriétés du composant WebPart

Dans les étapes précédentes, vous avez migré les solutions Calendrier des tâches du composant WebPart Éditeur de script vers SharePoint Framework. Bien que la solution fonctionne déjà comme prévu, elle n’utilise aucun des avantages de SharePoint Framework. Le nom de la liste à partir de laquelle les tâches sont chargées est inclus dans le code et le code lui-même est du JavaScript simple, qui est plus difficile à refactoriser que TypeScript.

Les étapes suivantes montrent comment étendre la solution existante pour permettre aux utilisateurs de spécifier le nom de la liste à partir de laquelle charger les données. Vous allez ensuite transformer le code en TypeScript pour utiliser ses fonctionnalités de sécurité des types.

Définir la propriété du composant WebPart pour stocker le nom de la liste

  1. Définissez une propriété de composant WebPart pour stocker le nom de la liste à partir de laquelle les tâches doivent être chargées. Dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.manifest.json, puis renommez la propriété par défaut description à listName et effacez sa valeur.

  2. Mettez à jour l’interface des propriétés du composant WebPart pour refléter les changements dans le manifeste. Dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/ITasksCalendarWebPartProps.ts et modifiez son contenu comme suit :

    export interface ITasksCalendarWebPartProps {
      listName: string;
    }
    
  3. Mettez à jour les étiquettes d’affichage de la propriété listName.

    Ouvrez le fichier ./src/webparts/tasksCalendar/loc/mystrings.d.ts et modifiez son contenu comme suit :

    declare interface ITasksCalendarStrings {
      PropertyPaneDescription: string;
      BasicGroupName: string;
      ListNameFieldLabel: string;
    }
    
    declare module 'tasksCalendarStrings' {
      const strings: ITasksCalendarStrings;
      export = strings;
    }
    
  4. Ouvrez le fichier ./src/webparts/tasksCalendar/loc/en-us.js et modifiez son contenu comme suit :

    define([], function() {
      return {
        "PropertyPaneDescription": "Tasks calendar settings",
        "BasicGroupName": "Data",
        "ListNameFieldLabel": "List name"
      }
    });
    
  5. Mettez à jour le composant WebPart pour utiliser la propriété nouvellement définie. Dans l’éditeur de code, ouvrez le fichier ./src/webparts/listInfo/ListInfoWebPart.ts et modifiez la méthode getPropertyPaneConfiguration comme suit :

    export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> {
      // ...
      protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
        return {
          pages: [
            {
              header: {
                description: strings.PropertyPaneDescription
              },
              groups: [
                {
                  groupName: strings.BasicGroupName,
                  groupFields: [
                    PropertyPaneTextField('listName', {
                      label: strings.ListNameFieldLabel
                    })
                  ]
                }
              ]
            }
          ]
        };
      }
    
      protected get disableReactivePropertyChanges(): boolean {
        return true;
      }
    }
    

Pour éviter que le composant WebPart soit rechargé lorsque les utilisateurs tapent le nom de la liste, vous avez également configuré le composant WebPart pour qu’il utilise le volet de propriétés non réactif en ajoutant la méthode disableReactivePropertyChanges() et en définissant sa valeur renvoyée sur true.

Utiliser le nom configuré de la liste à partir de laquelle charger les données

Initialement, le nom de la liste à partir de laquelle les données doivent être chargées était incorporé dans les requêtes REST. Maintenant que les utilisateurs peuvent configurer ce nom, la valeur configurée doit être injectée dans les requêtes REST avant de les exécuter Le moyen le plus simple de procéder consiste à déplacer le contenu du fichier script.js vers le fichier du composant WebPart principal.

  1. Dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts.

  2. Recherchez le code suivant que vous avez précédemment ajouté au fichier :

    import 'jquery';
    import 'moment';
    import 'fullcalendar';
    

    Mettez à jour le code avec le code suivant :

    var $: any = require('jquery');
    var moment: any = require('moment');
    
    import 'fullcalendar';
    
    var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
    

    Étant donné que Moment.js est référencé dans le code que vous utiliserez ultérieurement, son nom doit être connu de TypeScript afin d’éviter l’échec de la création du projet. Il en va de même pour jQuery.

    Étant donné que FullCalendar est un plug-in jQuery qui s’attache à l’objet jQuery, vous n’avez pas besoin de modifier son importation.

    La dernière partie comprend la copie de la liste des couleurs à utiliser pour marquer les différents événements.

  3. Copiez les fonctions displayTasks() et updateTask() à partir du fichier script.js , puis collez-les comme suit dans la classe TasksCalendarWebPart :

    export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> {
      // ...
    
      private displayTasks() {
        $('#calendar').fullCalendar('destroy');
        $('#calendar').fullCalendar({
          weekends: false,
          header: {
            left: 'prev,next today',
            center: 'title',
            right: 'month,basicWeek,basicDay'
          },
          displayEventTime: false,
          // open up the display form when a user clicks on an event
          eventClick: (calEvent, jsEvent, view) => {
            (window as any).location = this.context.pageContext.web.absoluteUrl +
              "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id;
          },
          editable: true,
          timezone: "UTC",
          droppable: true, // this allows things to be dropped onto the calendar
          // update the end date when a user drags and drops an event
          eventDrop: (event, delta, revertFunc) => {
            this.updateTask(event.id, event.start, event.end);
          },
          // put the events on the calendar
          events: (start, end, timezone, callback) => {
            var startDate = start.format('YYYY-MM-DD');
            var endDate = end.format('YYYY-MM-DD');
    
            var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\
    Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
    $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
    
            $.ajax({
              url: this.context.pageContext.web.absoluteUrl + restQuery,
              type: "GET",
              dataType: "json",
              headers: {
                Accept: "application/json;odata=nometadata"
              }
            })
              .done((data, textStatus, jqXHR) => {
                var personColors = {};
                var colorNo = 0;
    
                var events = data.value.map((task) => {
                  var assignedTo = task.AssignedTo.map((person) => {
                    return person.Title;
                  }).join(', ');
    
                  var color = personColors[assignedTo];
                  if (!color) {
                    color = COLORS[colorNo++];
                    personColors[assignedTo] = color;
                  }
                  if (colorNo >= COLORS.length) {
                    colorNo = 0;
                  }
    
                  return {
                    title: task.Title + " - " + assignedTo,
                    id: task.ID,
                    color: color, // specify the background color and border color can also create a class and use className parameter.
                    start: moment.utc(task.StartDate).add("1", "days"),
                    end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
                  };
                });
    
                callback(events);
              });
          }
        });
      }
    
      private updateTask(id, startDate, dueDate) {
        // subtract the previously added day to the date to store correct date
        var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
          startDate.format("hh:mm") + ":00Z";
        if (!dueDate) {
          dueDate = startDate;
        }
        var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
          dueDate.format("hh:mm") + ":00Z";
    
        $.ajax({
          url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
          type: 'POST',
          headers: {
            'Accept': 'application/json;odata=nometadata'
          }
        })
          .then((data, textStatus, jqXHR) => {
            return $.ajax({
              url: this.context.pageContext.web.absoluteUrl +
              "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")",
              type: 'POST',
              data: JSON.stringify({
                StartDate: sDate,
                DueDate: dDate,
              }),
              headers: {
                Accept: "application/json;odata=nometadata",
                "Content-Type": "application/json;odata=nometadata",
                "X-RequestDigest": data.FormDigestValue,
                "IF-MATCH": "*",
                "X-Http-Method": "PATCH"
              }
            });
          })
          .done((data, textStatus, jqXHR) => {
            alert("Update Successful");
          })
          .fail((jqXHR, textStatus, errorThrown) => {
            alert("Update Failed");
          })
          .always(() => {
            this.displayTasks();
          });
      // ...
    }
    

    Il y a quelques changements dans le code par rapport à la situation précédente.

    • Les fonctions JavaScript simples sont maintenant modifiées en méthodes TypeScript en remplaçant le mot-clé function par le modificateur private. Cela est nécessaire pour pouvoir les ajouter à la classe TaskCalendarWebPart.
    • Étant donné que ces deux méthodes sont maintenant dans le même fichier que le composant WebPart, au lieu de définir une variable générale contenant l’URL du site actuel, vous pouvez y accéder directement à partir du contexte du composant WebPart à l’aide de la propriété this.context.pageContext.web.absoluteUrl.
    • Dans toutes les requêtes REST, le nom de la liste fixe est remplacé par la valeur de la propriété listName qui contient le nom de la liste tel que configuré par l’utilisateur. Avant d’utiliser la valeur, elle est placée dans une séquence d’échappement à l’aide de la fonction escape() Lodash pour désactiver l’injection de scripts.
  4. La dernière étape consiste à modifier la méthode render() pour appeler la méthode displayTasks() qui vient d’être ajoutée :

    export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> {
      public render(): void {
        this.domElement.innerHTML = `
          <div class="${styles.tasksCalendar}">
            <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
            <div id="calendar"></div>
          </div>`;
    
        this.displayTasks();
      }
      // ...
    }
    
  5. Comme vous venez de déplacer le contenu du fichier script.js dans le fichier du composant WebPart principal, script.js n’est plus nécessaire et vous pouvez le supprimer du projet.

  6. Pour vérifier que le composant WebPart fonctionne comme prévu, exécutez la ligne de commande suivante :

    gulp serve --nobrowser
    
  7. Accédez au Workbench hébergé et ajoutez le composant WebPart au canevas. Ouvrez le volet de propriétés du composant WebPart, spécifiez le nom de la liste des tâches et cliquez sur le bouton Appliquer pour confirmer les modifications. Vous devez maintenant voir les tâches présentées dans un affichage Calendrier dans le composant WebPart.

    Tâches chargées de la liste configurée et affichées dans un composant WebPart SharePoint Framework côté client

Transformer le code JavaScript simple en TypeScript

L’utilisation de TypeScript à la place du code JavaScript simple présente de nombreux avantages. Non seulement parce que TypeScript est plus facile à gérer et à refactoriser, mais aussi car il vous permet de détecter des erreurs plus rapidement. Les étapes suivantes décrivent comment transformer le code JavaScript d’origine en TypeScript.

Ajouter des déclarations de type pour les bibliothèques utilisées

Pour fonctionner correctement, la fonction dactylographié requiert des déclarations de type pour les différentes bibliothèques utilisées dans le projet. Les déclarations de type sont souvent distribuées en tant que packages npm dans l’espace de @types noms .

  1. Installez les déclarations de type pour jQuery en exécutant ce qui suit dans la ligne de commande :

    npm install @types/jquery@1 --save-dev
    

    Les déclarations de types pour Moment.js sont distribuées avec le package Moment.js. Même si vous chargez Moment.js à partir d’une URL, pour pouvoir utiliser ses typages, vous devez installer le package Moment.js dans le projet.

  2. Installez le package Moment.js en exécutant ce qui suit dans la ligne de commande :

    npm install moment --save
    

Remarque

Vous remarquerez que nous n’installons pas de déclaration de type pour la bibliothèque FullCalendar. La solution démarrée par cet article utilise une ancienne version de FullCalendar qui n’ont pas de déclarations de type valides pour le moment. par conséquent, nous allons seulement utiliser les types de dactylographiés de façon sélective dans notre solution.

Idéalement, vous envisagez de mettre à niveau le projet vers une nouvelle version de la bibliothèque FullCalendar. Au-delà de l’objectif de cet article, les nombreuses modifications apportées à la version de l’API sont basées sur la version de notre solution de démarrage.

Mettre à jour les références de package

Pour utiliser des types provenant des déclarations de type installées, vous devez modifier la façon dont vous référencez les bibliothèques.

  1. Dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts

  2. Recherchez le code suivant que vous avez précédemment ajouté au fichier :

    var $: any = require('jquery');
    var moment: any = require('moment');
    
    import 'fullcalendar';
    

    Mettez à jour le code avec le code suivant :

    import * as $ from 'jquery';
    import 'fullcalendar';
    import * as moment from 'moment';
    

Mettre à jour les fichiers de composants WebPart principaux en TypeScript

Maintenant que vous avez des déclarations de type pour toutes les bibliothèques installées dans le projet, vous pouvez commencer à transformer le code JavaScript simple en TypeScript.

  1. Définissez une interface pour une tâche que vous récupérez dans la liste SharePoint. Dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts et juste au-dessus de la classe de composant WebPart, ajoutez l’extrait de code suivant :

    interface ITask {
      ID: number;
      Title: string;
      StartDate: string;
      DueDate: string;
      AssignedTo: [{ Title: string }];
    }
    
  2. Dans la classe de composant WebPart, modifiez la méthode displayTasks()et updateTask() comme suit :

    private displayTasks() {
      ($('#calendar') as any).fullCalendar('destroy');
      ($('#calendar') as any).fullCalendar({
        weekends: false,
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,basicWeek,basicDay'
        },
        displayEventTime: false,
        // open up the display form when a user clicks on an event
        eventClick: (calEvent: any, jsEvent: MouseEvent, view: any): void => {
          (window as any).location = this.context.pageContext.web.absoluteUrl +
            "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id;
        },
        editable: true,
        timezone: "UTC",
        droppable: true, // this allows things to be dropped onto the calendar
        // update the end date when a user drags and drops an event
        eventDrop: (event: any, delta: moment.Duration, revertFunc: Function): void => {
          this.updateTask(<number>event.id, <moment.Moment>event.start, <moment.Moment>event.end);
        },
        // put the events on the calendar
        events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => {
          var startDate = start.format('YYYY-MM-DD');
          var endDate = end.format('YYYY-MM-DD');
    
          var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\
      Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
      $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
    
          $.ajax({
            url: this.context.pageContext.web.absoluteUrl + restQuery,
            type: "GET",
            dataType: "json",
            headers: {
              Accept: "application/json;odata=nometadata"
            }
          })
            .done((data: { value: ITask[] }, textStatus: string, jqXHR: JQueryXHR) => {
              let personColors: { [person: string]: string; } = {};
              var colorNo = 0;
    
              var events = data.value.map((task: ITask): any => {
                var assignedTo = task.AssignedTo.map((person: { Title: string }): string => {
                  return person.Title;
                }).join(', ');
    
                var color = personColors[assignedTo];
                if (!color) {
                  color = COLORS[colorNo++];
                  personColors[assignedTo] = color;
                }
                if (colorNo >= COLORS.length) {
                  colorNo = 0;
                }
    
                return {
                  title: task.Title + " - " + assignedTo,
                  id: task.ID,
                  color: color, // specify the background color and border color can also create a class and use className parameter.
                  start: moment.utc(task.StartDate).add("1", "days"),
                  end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
                };
              });
    
              callback(events);
            });
        }
      });
    }
    
    private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment) {
      // subtract the previously added day to the date to store correct date
      var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        startDate.format("hh:mm") + ":00Z";
      if (!dueDate) {
        dueDate = startDate;
      }
      var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        dueDate.format("hh:mm") + ":00Z";
    
      $.ajax({
        url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
        type: 'POST',
        headers: {
          'Accept': 'application/json;odata=nometadata'
        }
      })
        .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => {
          return $.ajax({
            url: this.context.pageContext.web.absoluteUrl +
              "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")",
            type: 'POST',
            data: JSON.stringify({
              StartDate: sDate,
              DueDate: dDate,
            }),
            headers: {
              Accept: "application/json;odata=nometadata",
              "Content-Type": "application/json;odata=nometadata",
              "X-RequestDigest": data.FormDigestValue,
              "IF-MATCH": "*",
              "X-Http-Method": "PATCH"
            }
          });
        })
        .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => {
          alert("Update Successful");
        })
        .fail((jqXHR: JQueryXHR, textStatus: string, errorThrown: string) => {
          alert("Update Failed");
        })
        .always((): void => {
          this.displayTasks();
        });
      // ...
    }
    

Les types explicites sont les premiers changements lors de la transformation de JavaScript simple en TypeScript. Ils ne sont pas obligatoires mais ils permettent de savoir quel type de données est attendu. Tout écart est intercepté immédiatement par TypeScript qui vous permet de rechercher des problèmes éventuels lors du processus de développement. Ceci est utile lorsque vous travaillez avec des réponses AJAX et leurs données.

Un autre changement que vous aurez peut-être déjà remarqué est l’interpolation de chaîne TypeScript. L’interpolation de chaîne simplifie la composition de chaîne dynamique et augmente la lisibilité de votre code.

Comparez le code JavaScript simple :

var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";

avec :

const restQuery: string = `/_api/Web/Lists/GetByTitle('${escape(this.properties.listName)}')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '${startDate}' and DueDate le '${endDate}')or(StartDate ge '${startDate}' and StartDate le '${endDate}'))`;

L’interpolation de chaîne TypeScript évite aussi de placer les guillemets dans une séquence d’échappement, ce qui simplifie la composition des requêtes REST.

  1. Pour vérifier que tout fonctionne comme prévu, exécutez le code suivant dans la ligne de commande :

    gulp serve --nobrowser
    
  2. Accédez au Workbench hébergé et ajoutez le composant WebPart au canevas. Bien que rien n’ait changé visuellement, la nouvelle base de code utilise TypeScript et ses déclarations de type pour vous aider à gérer la solution.

Remplacer les appels AJAX jQuery avec l’API Framework SharePoint

La solution utilise actuellement les appels jQuery AJAX pour communiquer avec l’API REST SharePoint. Pour les demandes GET standard, l’API jQuery AJAX est aussi pratique que l’utilisation de l’API SharePoint Framework SPHttpClient. La vraie différence réside dans le fait que des demandes de publication telles que celle de mise à jour de l’événement :

$.ajax({
  url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
  type: 'POST',
  headers: { 'Accept': 'application/json;odata=nometadata'}
})
  .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => {
    return $.ajax({
      url: `${this.context.pageContext.web.absoluteUrl}\
/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`,
      type: 'POST',
      data: JSON.stringify({
        StartDate: sDate,
        DueDate: dDate,
      }),
      headers: {
        Accept: "application/json;odata=nometadata",
        "Content-Type": "application/json;odata=nometadata",
        "X-RequestDigest": data.FormDigestValue,
        "IF-MATCH": "*",
        "X-Http-Method": "PATCH"
      }
    });
  })
  .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => {
    alert("Update Successful");
  });
  // ...

Étant donné que vous souhaitez mettre à jour un élément de liste, vous devez fournir à SharePoint un jeton digest de demande valide. Bien que le résumé soit disponible sur les pages classiques, sa validité est de 3 minutes uniquement. Par conséquent, il est toujours plus sûr de récupérer un jeton valide vous-même avant d’effectuer une opération de mise à jour. Une fois que vous avez obtenu le digest de demande, vous devez l’ajouter aux en-têtes de demande de la demande de mise à jour. Sinon, la demande échoue.

SPHttpClient de SharePoint Framework simplifie la communication avec SharePoint, car il détecte automatiquement si la demande est une demande POST nécessitant un digest de demande valide. Si c’est le cas, l’API SPHttpClient le récupère automatiquement auprès de SharePoint et l’ajoute à la demande. À titre de comparaison, la même demande émise à l’aide de l’API SPHttpClient se présente comme suit :

this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\
/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, {
  body: JSON.stringify({
    StartDate: sDate,
    DueDate: dDate,
  }),
  headers: {
    Accept: "application/json;odata=nometadata",
    "Content-Type": "application/json;odata=nometadata",
    "IF-MATCH": "*",
    "X-Http-Method": "PATCH"
  }
})
.then((response: SPHttpClientResponse): void => {
  // ...
});
  1. Pour remplacer les appels AJAX jQuery d’origine avec l’API SPHttpClient de SharePoint Framework, dans l’éditeur de code, ouvrez le fichier ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts. Ajoutez les instructions suivantes à la liste des instructions import existantes :

    import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
    
  2. Dans la classe TasksCalendarWebPart , remplacez les méthodes displayTasks() et updateTask() par le code suivant :

    private displayTasks() {
      ($('#calendar') as any).fullCalendar('destroy');
      ($('#calendar') as any).fullCalendar({
        weekends: false,
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,basicWeek,basicDay'
        },
        displayEventTime: false,
        // open up the display form when a user clicks on an event
        eventClick: (calEvent: any, jsEvent: MouseEvent, view: any): void => {
          (window as any).location = this.context.pageContext.web.absoluteUrl +
            "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id;
        },
        editable: true,
        timezone: "UTC",
        droppable: true, // this allows things to be dropped onto the calendar
        // update the end date when a user drags and drops an event
        eventDrop: (event: any, delta: moment.Duration, revertFunc: Function): void => {
          this.updateTask(<number>event.id, <moment.Moment>event.start, <moment.Moment>event.end);
        },
        // put the events on the calendar
        events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => {
          var startDate = start.format('YYYY-MM-DD');
          var endDate = end.format('YYYY-MM-DD');
    
          var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\
      Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
      $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
    
          this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + restQuery, SPHttpClient.configurations.v1, {
            headers: {
              'Accept': "application/json;odata.metadata=none"
            }
          })
            .then((response: SPHttpClientResponse): Promise<{ value: ITask[] }> => {
              return response.json();
            })
            .then((data: { value: ITask[] }): void => {
              let personColors: { [person: string]: string; } = {};
              var colorNo = 0;
    
              var events = data.value.map((task: ITask): any => {
                var assignedTo = task.AssignedTo.map((person: { Title: string }): string => {
                  return person.Title;
                }).join(', ');
    
                var color = personColors[assignedTo];
                if (!color) {
                  color = COLORS[colorNo++];
                  personColors[assignedTo] = color;
                }
                if (colorNo >= COLORS.length) {
                  colorNo = 0;
                }
    
                return {
                  title: task.Title + " - " + assignedTo,
                  id: task.ID,
                  color: color, // specify the background color and border color can also create a class and use className parameter.
                  start: moment.utc(task.StartDate).add("1", "days"),
                  end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
                };
              });
    
              callback(events);
            });
        }
      });
    }
    
    private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment) {
      // subtract the previously added day to the date to store correct date
      var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        startDate.format("hh:mm") + ":00Z";
      if (!dueDate) {
        dueDate = startDate;
      }
      var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
        dueDate.format("hh:mm") + ":00Z";
    
      this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\
    /_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, {
        body: JSON.stringify({
          StartDate: sDate,
          DueDate: dDate,
        }),
        headers: {
          Accept: "application/json;odata=nometadata",
          "Content-Type": "application/json;odata=nometadata",
          "IF-MATCH": "*",
          "X-Http-Method": "PATCH"
        }
      })
        .then((response: SPHttpClientResponse): void => {
          if (response.ok) {
            alert("Update Successful");
          } else {
            alert("Update Failed");
          }
    
          this.displayTasks();
        });
    }
    

    Importante

    Si vous supprimez des métadonnées dans les réponses de l’API REST de SharePoint, lorsque vous utilisez le SPHttpClient de SharePoint Framework, vous devez vous assurer que vous utilisez application/json;odata.metadata=none et non application/json;odata=nometadata comme valeur de l’en-tête Accepter. SPHttpClient utilise OData 4.0 et requiert la première valeur. Si vous utilisez l’autre valeur à la place, la demande échoue avec une réponse 406 - Non acceptable.

  3. Pour vérifier que tout fonctionne comme prévu, exécutez le code suivant dans la ligne de commande :

    gulp serve --nobrowser
    
  4. Accédez au Workbench hébergé et ajoutez le composant WebPart au canevas. Bien qu’il n’existe toujours aucune modification visuelle, le nouveau code utilise le SPHttpClient de SharePoint Framework qui simplifie votre code et la maintenance de votre solution.