Créer, mettre à jour et supprimer des fichiers dans un conteneur

Effectué

Dans cet exercice, vous allez mettre à jour le projet existant pour stocker des fichiers dans un conteneur SharePoint Embedded.

Ajouter des déclarations de type Microsoft Graph au projet

Avant de créer les nouveaux composants React, commençons par mettre à jour le projet.

Tout au long de cet exercice, nous allons utiliser les types fournis par Microsoft Graph. Étant donné que nous n’avons pas installé le package npm qui les inclut, nous devons d’abord le faire.

À partir de la ligne de commande, exécutez la commande suivante à partir du dossier racine de notre projet :

npm install @microsoft/microsoft-graph-types -DE

Mettre à jour le composant React Containers pour afficher les fichiers

Rappelez-vous que dans l’exercice précédent, nous avons laissé un espace réservé dans notre composant Conteneurs qui sera utilisé pour afficher le contenu du conteneur sélectionné.

Nous n’avons pas créé notre Files composant, mais commençons par mettre à jour le Containers composant pour remplacer l’espace réservé par le Files composant que nous allons créer.

Recherchez et ouvrez le fichier ./src/components/containers.tsx .

Ajoutez l’instruction import suivante à la liste des importations existantes en haut du fichier :

import { Files } from "./files";

Ensuite, recherchez le code d’espace réservé suivant près de la fin du fichier...

{selectedContainer && (`[[TOOD]] container "${selectedContainer.displayName}" contents go here`)}

… et remplacez-le par le code suivant :

{selectedContainer && (<Files container={selectedContainer} />)}

Créer le composant Files React

Commençons par créer un composant React pour afficher et gérer le contenu des conteneurs.

Créez un nouveau fichier, ./src/components/files.tsx, puis ajoutez-y le code suivant. Il s’agit du composant réutilisable qui inclut toutes les importations et le squelette de notre composant :

import React, {
  useState,
  useEffect,
  useRef
} from 'react';
import { Providers } from "@microsoft/mgt-element";
import {
  AddRegular, ArrowUploadRegular,
  FolderRegular, DocumentRegular,
  SaveRegular, DeleteRegular,
} from '@fluentui/react-icons';
import {
  Button, Link, Label, Spinner,
  Input, InputProps, InputOnChangeData,
  Dialog, DialogActions, DialogContent, DialogBody, DialogSurface, DialogTitle, DialogTrigger,
  DataGrid, DataGridProps,
  DataGridHeader, DataGridHeaderCell,
  DataGridBody, DataGridRow,
  DataGridCell,
  TableColumnDefinition, createTableColumn,
  TableRowId,
  TableCellLayout,
  OnSelectionChangeData,
  SelectionItemId,
  Toolbar, ToolbarButton,
  makeStyles
} from "@fluentui/react-components";
import {
  DriveItem
} from "@microsoft/microsoft-graph-types-beta";
import { IContainer } from "./../common/IContainer";
require('isomorphic-fetch');

interface IFilesProps {
  container: IContainer;
}

interface IDriveItemExtended extends DriveItem {
  isFolder: boolean;
  modifiedByName: string;
  iconElement: JSX.Element;
  downloadUrl: string;
}

export const Files = (props: IFilesProps) => {

  // BOOKMARK 1 - constants & hooks

  // BOOKMARK 2 - handlers go here

  // BOOKMARK 3 - component rendering return (
  return
  (
    <div>
    </div>
  );
}

export default Files;

Remarque

Notez les // BOOKMARK # commentaires dans le composant . Ceux-ci pour vous assurer que vous ajoutez du code aux emplacements appropriés.

Afficher la liste du contenu du conteneur sélectionné

La première chose que nous devons traiter est d’afficher le contenu du conteneur sélectionné. Pour ce faire, nous allons utiliser le DataGrid composant de la bibliothèque React de l’interface utilisateur Fluent.

Ajoutez le balisage suivant à l’intérieur de l’élément <div> dans l’instruction return() après le // BOOKMARK 3 commentaire :

<DataGrid
  items={driveItems}
  columns={columns}
  getRowId={(item) => item.id}
  resizableColumns
  columnSizingOptions={columnSizingOptions}
  >
  <DataGridHeader>
    <DataGridRow>
      {({ renderHeaderCell }) => (
        <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
      )}
    </DataGridRow>
  </DataGridHeader>
  <DataGridBody<IDriveItemExtended>>
    {({ item, rowId }) => (
      <DataGridRow<IDriveItemExtended> key={rowId}>
        {({ renderCell, columnId }) => (
          <DataGridCell>
            {renderCell(item)}
          </DataGridCell>
        )}
      </DataGridRow>
    )}
  </DataGridBody>
</DataGrid>

le DataGrid contient quelques références à des collections, des paramètres et des méthodes que nous devons configurer. Commençons par les éléments visuels, puis nous obtiendrons les données.

Les colonnes de peuvent DataGrid être redimensionnées en fonction des propriétés que nous définissons. Créez une nouvelle constante, columnSizingOptions, et ajoutez le code juste avant le // BOOKMARK 3 commentaire :

const columnSizingOptions = {
  driveItemName: {
    minWidth: 150,
    defaultWidth: 250,
    idealWidth: 200
  },
  lastModifiedTimestamp: {
    minWidth: 150,
    defaultWidth: 150
  },
  lastModifiedBy: {
    minWidth: 150,
    defaultWidth: 150
  },
  actions: {
    minWidth: 250,
    defaultWidth: 250
  }
};

Ensuite, définissez les paramètres de structure et de rendu de toutes les colonnes dans .DataGrid Pour ce faire, créez une collection, columnset ajoutez-la immédiatement avant le columnSizingOptions que vous avez créé :

const columns: TableColumnDefinition<IDriveItemExtended>[] = [
  createTableColumn({
    columnId: 'driveItemName',
    renderHeaderCell: () => {
      return 'Name'
    },
    renderCell: (driveItem) => {
      return (
        <TableCellLayout media={driveItem.iconElement}>
          <Link href={driveItem!.webUrl!} target='_blank'>{driveItem.name}</Link>
        </TableCellLayout>
      )
    }
  }),
  createTableColumn({
    columnId: 'lastModifiedTimestamp',
    renderHeaderCell: () => {
      return 'Last Modified'
    },
    renderCell: (driveItem) => {
      return (
        <TableCellLayout>
          {driveItem.lastModifiedDateTime}
        </TableCellLayout>
      )
    }
  }),
  createTableColumn({
    columnId: 'lastModifiedBy',
    renderHeaderCell: () => {
      return 'Last Modified By'
    },
    renderCell: (driveItem) => {
      return (
        <TableCellLayout>
          {driveItem.modifiedByName}
        </TableCellLayout>
      )
    }
  }),
  createTableColumn({
    columnId: 'actions',
    renderHeaderCell: () => {
      return 'Actions'
    },
    renderCell: (driveItem) => {
      return (
        <>
          <Button aria-label="Download"
            disabled={!selectedRows.has(driveItem.id as string)}
            icon={<SaveRegular />}>Download</Button>
          <Button aria-label="Delete"
            icon={<DeleteRegular />}>Delete</Button>
        </>
      )
    }
  }),
];

Ce code utilise la méthode createTableColumn() utilitaire pour attribuer à chaque colonne un ID et spécifier le rendu des cellules d’en-tête et de corps dans le tableau.

Une fois configuré DataGrid , ajoutez les constantes suivantes pour gérer l’état de l’application React avec les propriétés que notre code existant utilise. Ajoutez le code suivant juste avant le commentaire // BOOKMARK 1 :

const [driveItems, setDriveItems] = useState<IDriveItemExtended[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<SelectionItemId>>(new Set<TableRowId>([1]));

À présent, nous allons ajouter des gestionnaires pour extraire et afficher les données de notre conteneur.

Ajoutez le gestionnaire et le hook React suivants pour obtenir le contenu du conteneur sélectionné. Le useEffect hook s’exécute la première fois que le composant est rendu, ainsi que lorsque les <Files />propriétés d’entrée du composant changent.

Ajoutez le code suivant juste avant le commentaire // BOOKMARK 2 :

useEffect(() => {
  (async () => {
    loadItems();
  })();
}, [props]);

const loadItems = async (itemId?: string) => {
  try {
    const graphClient = Providers.globalProvider.graph.client;
    const driveId = props.container.id;
    const driveItemId = itemId || 'root';

    // get Container items at current level
    const graphResponse = await graphClient.api(`/drives/${driveId}/items/${driveItemId}/children`).get();
    const containerItems: DriveItem[] = graphResponse.value as DriveItem[]
    const items: IDriveItemExtended[] = [];
    containerItems.forEach((driveItem: DriveItem) => {
      items.push({
        ...driveItem,
        isFolder: (driveItem.folder) ? true : false,
        modifiedByName: (driveItem.lastModifiedBy?.user?.displayName) ? driveItem.lastModifiedBy!.user!.displayName : 'unknown',
        iconElement: (driveItem.folder) ? <FolderRegular /> : <DocumentRegular />,
        downloadUrl: (driveItem as any)['@microsoft.graph.downloadUrl']
      });
    });
    setDriveItems(items);
  } catch (error: any) {
    console.error(`Failed to load items: ${error.message}`);
  }
};

La loadItems fonction utilise le client Microsoft Graph pour obtenir la liste de tous les fichiers du dossier actif, en utilisant par défaut si root aucun dossier n’est déjà sélectionné.

Il prend ensuite la collection de DriveItems retournée par Microsoft Graph et ajoute quelques propriétés supplémentaires pour simplifier notre code ultérieurement. À la fin de la méthode, elle appelle la setDriveitems() méthode d’accesseur d’état qui déclenchera un rendu du composant. Sont driveItems définis sur la DataGrid.items propriété qui explique pourquoi le tableau affiche des informations.

Tester le rendu de la liste du contenu d’un conteneur

Nous allons maintenant tester l’application React côté client pour vérifier que le <Files /> composant affiche le contenu du conteneur sélectionné.

À partir de la ligne de commande dans le dossier racine du projet, exécutez la commande suivante :

npm run start

Lorsque le navigateur se charge, connectez-vous à l’aide du même compte professionnel et scolaire que celui que vous avez utilisé.

Une fois connecté, sélectionnez un conteneur existant. Si ce conteneur contient déjà du contenu, il s’affiche comme suit :

Capture d’écran du DataGrid de base affichant le contenu de notre conteneur.

Si vous sélectionnez le fichier, dans ce cas un document Word, un nouvel onglet s’ouvre et charge l’URL de l’élément. Pour cet exemple, le fichier est ouvert dans Word Online.

Ajout de la prise en charge du téléchargement de fichiers

Une fois la fonctionnalité d’affichage du contenu terminée, nous allons mettre à jour le composant pour prendre en charge le téléchargement de fichiers.

Commencez par ajouter le code suivant immédiatement avant le // BOOKMARK 1 commentaire :

const downloadLinkRef = useRef<HTMLAnchorElement>(null);

Ensuite, nous voulons nous assurer qu’un élément du est sélectionné avant de pouvoir le DataGrid télécharger. Sinon, le bouton Télécharger est désactivé tel qu’il est actuellement.

Dans , DataGridajoutez trois propriétés pour le définir pour prendre en charge un mode de sélection d’élément unique (selectionMode), suivre les éléments sélectionnés (selectedItems) et ce qu’il faut faire quand la sélection change (onSelectionChange).

<DataGrid
  ...
  selectionMode='single'
  selectedItems={selectedRows}
  onSelectionChange={onSelectionChange}>

Ensuite, ajoutez le gestionnaire suivant immédiatement avant le // BOOKMARK 2 commentaire :

const onSelectionChange: DataGridProps["onSelectionChange"] = (event: React.MouseEvent | React.KeyboardEvent, data: OnSelectionChangeData): void => {
  setSelectedRows(data.selectedItems);
}

Maintenant, lorsqu’un élément de la liste est sélectionné, le bouton Télécharger n’est plus désactivé.

Capture d’écran du bouton Télécharger activé lorsqu’un élément est sélectionné.

L’option de téléchargement utilise un lien hypertexte masqué que nous allons d’abord définir par programmation le lien de téléchargement pour l’élément sélectionné, puis effectuer les opérations suivantes par programmation :

  1. Définissez l’URL du lien hypertexte sur l’URL de téléchargement de l’élément.
  2. Sélectionnez le lien hypertexte.

Cela déclenche le téléchargement pour l’utilisateur.

Ajoutez le balisage suivant juste après l’ouverture <div> dans la return() méthode :

<a ref={downloadLinkRef} href="" target="_blank" style={{ display: 'none' }} />

À présent, recherchez la constante existante columns que vous avez ajoutée précédemment et recherchez le createTableColumn qui fait référence à .columnId: 'actions' Dans la renderCell propriété , ajoutez un onClick gestionnaire qui appellera le onDownloadItemClick. Lorsque vous avez terminé, le bouton doit ressembler à ce qui suit :

<Button aria-label="Download"
        disabled={!selectedRows.has(driveItem.id as string)}
        icon={<SaveRegular />}
        onClick={() => onDownloadItemClick(driveItem.downloadUrl)}>Download</Button>

Enfin, ajoutez le gestionnaire suivant immédiatement après le gestionnaire d’événements existant onSelectionChange que vous avez ajouté précédemment. Cette opération gère les deux étapes de programmation mentionnées précédemment :

const onDownloadItemClick = (downloadUrl: string) => {
  const link = downloadLinkRef.current;
  link!.href = downloadUrl;
  link!.click();
}

Enregistrez vos modifications, actualisez le navigateur et sélectionnez le lien Télécharger pour voir le fichier téléchargé.

Capture d’écran de la nouvelle prise en charge du téléchargement du fichier.

Ajouter la possibilité de créer un dossier dans un conteneur

Continuons à générer le composant en ajoutant la <Files /> prise en charge de la création et de l’affichage des dossiers.

Commencez par ajouter le code suivant immédiatement avant le // BOOKMARK 1 commentaire. Cela ajoute les valeurs d’état React nécessaires que nous allons utiliser :

const [folderId, setFolderId] = useState<string>('root');
const [folderName, setFolderName] = useState<string>('');
const [creatingFolder, setCreatingFolder] = useState<boolean>(false);
const [newFolderDialogOpen, setNewFolderDialogOpen] = useState(false);

Pour créer un dossier, nous allons afficher une boîte de dialogue à l’utilisateur lorsqu’il sélectionne un bouton dans la barre d’outils.

Dans la return() méthode , immédiatement avant le <DataGrid>, ajoutez le code suivant pour implémenter la boîte de dialogue :

<Toolbar>
  <ToolbarButton vertical icon={<AddRegular />} onClick={() => setNewFolderDialogOpen(true)}>New Folder</ToolbarButton>
</Toolbar>

<Dialog open={newFolderDialogOpen}>
  <DialogSurface>
    <DialogBody>
      <DialogTitle>Create New Folder</DialogTitle>
      <DialogContent className={styles.dialogContent}>
        <Label htmlFor={folderName}>Folder name:</Label>
        <Input id={folderName} className={styles.dialogInputControl} autoFocus required
          value={folderName} onChange={onHandleFolderNameChange}></Input>
        {creatingFolder &&
          <Spinner size='medium' label='Creating folder...' labelPosition='after' />
        }
      </DialogContent>
      <DialogActions>
        <DialogTrigger disableButtonEnhancement>
          <Button appearance="secondary" onClick={() => setNewFolderDialogOpen(false)} disabled={creatingFolder}>Cancel</Button>
        </DialogTrigger>
        <Button appearance="primary"
          onClick={onFolderCreateClick}
          disabled={creatingFolder || (folderName === '')}>Create Folder</Button>
      </DialogActions>
    </DialogBody>
  </DialogSurface>
</Dialog>

La boîte de dialogue utilise certains styles personnalisés que nous n’avons pas encore ajoutés. Pour ce faire, ajoutez d’abord le code suivant juste avant la déclaration du Files composant :

const useStyles = makeStyles({
  dialogInputControl: {
    width: '400px',
  },
  dialogContent: {
    display: 'flex',
    flexDirection: 'column',
    rowGap: '10px',
    marginBottom: '25px'
  }
});

Ensuite, ajoutez le code suivant immédiatement avant la return() méthode dans notre composant :

const styles = useStyles();

Maintenant que l’interface utilisateur est configurée, nous devons maintenant ajouter des gestionnaires. Ajoutez les gestionnaires suivants immédiatement avant le // BOOKMARK 2 commentaire. Ceux-ci gèrent l’ouverture de la boîte de dialogue, l’enregistrement de la valeur du nom du nouveau dossier et ce qui se passe lorsqu’ils sélectionnent le bouton dans la boîte de dialogue :

const onFolderCreateClick = async () => {
  setCreatingFolder(true);

  const currentFolderId = folderId;
  const graphClient = Providers.globalProvider.graph.client;
  const endpoint = `/drives/${props.container.id}/items/${currentFolderId}/children`;
  const data = {
    "name": folderName,
    "folder": {},
    "@microsoft.graph.conflictBehavior": "rename"
  };
  await graphClient.api(endpoint).post(data);

  await loadItems(currentFolderId);

  setCreatingFolder(false);
  setNewFolderDialogOpen(false);
};

const onHandleFolderNameChange: InputProps["onChange"] = (event: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData): void => {
  setFolderName(data?.value);
};

Enregistrez vos modifications, actualisez le navigateur et sélectionnez le bouton Nouveau dossier au-dessus du contenu du conteneur :

Capture d’écran montrant le bouton Nouveau dossier.

Sélectionnez le bouton Nouveau dossier , entrez un nom, puis sélectionnez le bouton Créer un dossier dans la boîte de dialogue.

Capture d’écran de la boîte de dialogue Créer un dossier.

Lorsque le dossier est créé, vous le verrez répertorié dans la table de contenu :

Capture d’écran du nouveau dossier dans le conteneur.

Nous devons apporter une autre modification à notre composant. À l’heure actuelle, lorsque vous sélectionnez un dossier, l’URL est lancée dans un nouvel onglet en laissant l’application de côté. Ce n’est pas ce que nous voulons... nous voulons qu’il explore le dossier.

Nous allons résoudre ce problème en localisant la constante existante columns que vous avez ajoutée précédemment et en recherchant le createTableColumn qui fait référence à columnId: 'driveItemName'. Dans la renderCell propriété , remplacez le composant existant <Link /> par le code suivant. Deux liens sont générés en fonction du fait que l’élément actif affiché est un dossier ou un fichier :

{(!driveItem.isFolder)
  ? <Link href={driveItem!.webUrl!} target='_blank'>{driveItem.name}</Link>
  : <Link onClick={() => {
    loadItems(driveItem.id);
    setFolderId(driveItem.id as string)
  }}>{driveItem.name}</Link>
}

Maintenant, lorsque vous sélectionnez un dossier, l’application affiche le contenu du dossier.

Ajout de la possibilité de supprimer un fichier ou un dossier

L’étape suivante consiste à ajouter la possibilité de supprimer un dossier ou un fichier du conteneur.

Pour ce faire, commencez par ajouter le code suivant à la liste existante des useState() appels avant le // BOOKMARK 1 commentaire

const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);

Ensuite, ajoutez une boîte de dialogue pour servir de confirmation lorsque l’utilisateur sélectionne le bouton Supprimer . Ajoutez le code suivant juste après le composant existant <Dialog> dans la return() méthode :

<Dialog open={deleteDialogOpen} modalType='modal' onOpenChange={() => setSelectedRows(new Set<TableRowId>([0]))}>
  <DialogSurface>
    <DialogBody>
      <DialogTitle>Delete Item</DialogTitle>
      <DialogContent>
        <p>Are you sure you want to delete this item?</p>
      </DialogContent>
      <DialogActions>
        <DialogTrigger>
          <Button
            appearance='secondary'
            onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
        </DialogTrigger>
        <Button
          appearance='primary'
          onClick={onDeleteItemClick}>Delete</Button>
      </DialogActions>
    </DialogBody>
  </DialogSurface>
</Dialog>

Mettez à jour le bouton Supprimer pour effectuer une action lorsqu’il est sélectionné. Recherchez la constante existante columns que vous avez ajoutée précédemment et recherchez le createTableColumn qui fait référence au columnId: 'actions'. Dans la renderCell propriété , ajoutez un onClick gestionnaire qui appellera le onDeleteDialogOpen. Lorsque vous avez terminé, le bouton doit ressembler à ce qui suit :

<Button aria-label="Delete"
        icon={<DeleteRegular />}
        onClick={() => setDeleteDialogOpen(true)}>Delete</Button>

Enfin, ajoutez le code suivant juste avant le // BOOKMARK 2 commentaire pour gérer la suppression de l’élément actuellement sélectionné :

const onDeleteItemClick = async () => {
  const graphClient = Providers.globalProvider.graph.client;
  const endpoint = `/drives/${props.container.id}/items/${selectedRows.entries().next().value[0]}`;
  await graphClient.api(endpoint).delete();
  await loadItems(folderId || 'root');
  setDeleteDialogOpen(false);
}

Enregistrez vos modifications et actualisez le navigateur. Sélectionnez le bouton Supprimer sur l’un des dossiers ou fichiers existants dans votre collection. La boîte de dialogue de confirmation s’affiche et lorsque vous sélectionnez le bouton Supprimer dans la boîte de dialogue, la table du contenu du conteneur s’actualise pour indiquer que l’élément a été supprimé :

Capture d’écran montrant la fonctionnalité de suppression des éléments d’un conteneur.

Ajouter la possibilité de charger des fichiers dans le conteneur

La dernière étape consiste à ajouter la possibilité de charger des fichiers dans un conteneur ou un dossier au sein d’un conteneur.

Commencez par ajouter le code suivant immédiatement avant le // BOOKMARK 1 commentaire :

const uploadFileRef = useRef<HTMLInputElement>(null);

Ensuite, nous allons répéter une technique similaire à l’aide d’un contrôle masqué <Input> pour charger un fichier. Ajoutez le code suivant immédiatement après l’ouverture <div> dans la méthode du return() composant :

<input ref={uploadFileRef} type="file" onChange={onUploadFileSelected} style={{ display: 'none' }} />

Ajoutez un bouton à la barre d’outils pour déclencher la boîte de dialogue de sélection de fichier. Pour ce faire, ajoutez le code suivant immédiatement après le bouton de barre d’outils existant qui ajoute un nouveau dossier :

<ToolbarButton vertical icon={<ArrowUploadRegular />} onClick={onUploadFileClick}>Upload File</ToolbarButton>

Enfin, ajoutez le code suivant juste avant le // BOOKMARK 2 commentaire pour ajouter deux gestionnaires d’événements. Le onUploadFileClick gestionnaire est déclenché lorsque vous sélectionnez le bouton de la barre d’outils Charger un fichier et le onUploadFileSelected gestionnaire est déclenché lorsque l’utilisateur sélectionne un fichier :

const onUploadFileClick = () => {
  if (uploadFileRef.current) {
    uploadFileRef.current.click();
  }
};

const onUploadFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
  const file = event.target.files![0];
  const fileReader = new FileReader();
  fileReader.readAsArrayBuffer(file);
  fileReader.addEventListener('loadend', async (event: any) => {
    const graphClient = Providers.globalProvider.graph.client;
    const endpoint = `/drives/${props.container.id}/items/${folderId || 'root'}:/${file.name}:/content`;
    graphClient.api(endpoint).putStream(fileReader.result)
      .then(async (response) => {
        await loadItems(folderId || 'root');
      })
      .catch((error) => {
        console.error(`Failed to upload file ${file.name}: ${error.message}`);
      });
  });
  fileReader.addEventListener('error', (event: any) => {
    console.error(`Error on reading file: ${event.message}`);
  });
};

Testez les modifications en enregistrant le fichier, en actualisant le navigateur et en sélectionnant le bouton Charger un fichier :

Capture d’écran du bouton Charger un fichier.

Après avoir sélectionné un fichier, notre application charge le fichier et actualise la table du contenu du conteneur :

Capture d’écran du nouveau fichier affiché dans le conteneur.

Résumé

Dans cet exercice, vous avez mis à jour le projet existant pour stocker et gérer des fichiers dans un conteneur SharePoint Embedded.