Criar, atualizar e eliminar ficheiros num Contentor

Concluído

Neste exercício, irá atualizar o projeto existente para armazenar ficheiros num Contentor Do SharePoint Embedded.

Adicionar Declarações de Tipo do Microsoft Graph ao projeto

Antes de criarmos os novos componentes do React, vamos começar por atualizar o projeto.

Ao longo deste exercício, vamos utilizar tipos fornecidos pelo Microsoft Graph. Uma vez que não instalámos o pacote npm que os inclui, temos de o fazer primeiro.

Na linha de comandos, execute o seguinte comando a partir da pasta raiz do nosso projeto:

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

Atualizar o componente Contentores do React para apresentar ficheiros

Lembre-se do exercício anterior, deixámos um marcador de posição no componente Contentores que será utilizado para apresentar o conteúdo do Contentor selecionado.

Ainda não criámos o nosso Files componente, mas vamos começar por atualizar o Containers componente para substituir o marcador de posição pelo Files componente que vamos criar.

Localize e abra o ficheiro ./src/components/containers.tsx .

Adicione a seguinte instrução de importação à lista de importações existentes na parte superior do ficheiro:

import { Files } from "./files";

Em seguida, localize o seguinte código de marcador de posição perto do fim do ficheiro...

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

… e substitua-o pelo seguinte código:

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

Criar o componente Do React de Ficheiros

Vamos começar por criar um novo componente do React para apresentar e gerir os conteúdos dos Contentores.

Crie um novo ficheiro, ./src/components/files.tsx, e adicione o seguinte código. Este é o componente automático que inclui todas as importações e a estrutura do nosso componente:

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;

Observação

Repare nos // BOOKMARK # comentários no componente. Para garantir que está a adicionar código nos locais corretos.

Apresentar uma lista dos conteúdos do Contentor selecionados

A primeira coisa que temos de abordar é apresentar os conteúdos do Contentor selecionado. Para tal, vamos utilizar o DataGrid componente da biblioteca Fluent UI React.

Adicione a seguinte marcação dentro do <div> elemento na instrução return() após o // BOOKMARK 3 comentário:

<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>

Contém DataGrid algumas referências a coleções, definições e métodos que precisamos de configurar. Vamos começar com os elementos visuais e, em seguida, obter os dados.

As colunas no DataGrid podem ser redimensionadas de acordo com as propriedades que definimos. Crie uma nova constante, columnSizingOptionse adicione o código imediatamente antes do // BOOKMARK 3 comentário:

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

Em seguida, defina a estrutura e as definições de composição para todas as colunas no DataGrid. Faça-o ao criar uma nova coleção, columnse adicione-a imediatamente antes da columnSizingOptions criação:

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>
        </>
      )
    }
  }),
];

Este código utilizará o método createTableColumn() utilitário para dar um ID a cada coluna e especificar a forma como as células do cabeçalho e do corpo na tabela são compostas.

Com a DataGrid configuração, adicione as seguintes constantes para gerir o estado da aplicação React com as propriedades que o nosso código existente está a utilizar. Adicione o seguinte código imediatamente antes do comentário // BOOKMARK 1:

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

Agora, vamos adicionar alguns processadores para obter e apresentar os dados do nosso Contentor.

Adicione o seguinte processador e o hook do React para obter o conteúdo do Contentor selecionado. O useEffect hook será executado na primeira vez que o componente for composto, bem como quando as <Files />propriedades de entrada do componente forem alteradas.

Adicione o seguinte código imediatamente antes do comentário // 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}`);
  }
};

A loadItems função utiliza o cliente do Microsoft Graph para obter uma lista de todos os ficheiros na pasta atual, predefinindo para a root pasta se ainda não estiver selecionada nenhuma pasta.

Em seguida, utiliza a coleção de DriveItems devolvida pelo Microsoft Graph e adiciona mais algumas propriedades para simplificar o nosso código mais tarde. No final do método, chama o setDriveitems() método acessório de estado que irá acionar uma nova composição do componente. Os driveItems estão definidos na DataGrid.items propriedade que explica por que motivo a tabela apresenta algumas informações.

Testar a composição de listar os conteúdos de um Contentor

Agora, vamos testar a aplicação React do lado do cliente para garantir que o <Files /> componente está a apresentar os conteúdos do Contentor selecionado.

Na linha de comandos na pasta raiz do projeto, execute o seguinte comando:

npm run start

Quando o browser for carregado, inicie sessão com a mesma conta Escolar e Profissional que tem utilizado.

Após iniciar sessão, selecione um Contentor existente. Se esse Contentor já tiver algum conteúdo, será apresentado da seguinte forma:

Captura de ecrã do DataGrid básico a apresentar os conteúdos do nosso Contentor.

Se selecionar o ficheiro, neste caso, um documento do Word irá abrir um novo separador e carregar o URL do item. Neste exemplo, o ficheiro é aberto no Word online.

Adicionar suporte para transferir ficheiros

Com a funcionalidade de apresentação de conteúdos concluída, vamos atualizar o componente para suportar a transferência de ficheiros.

Comece por adicionar o seguinte código imediatamente antes do // BOOKMARK 1 comentário:

const downloadLinkRef = useRef<HTMLAnchorElement>(null);

Em seguida, queremos certificar-nos de que um item no DataGrid está selecionado antes de poder transferi-lo. Caso contrário, o botão Transferir será desativado tal como está atualmente.

DataGridNo , adicione três propriedades para defini-lo para suportar um único modo de seleção de itens (selectionMode), controlar os itens selecionados (selectedItems) e o que fazer quando a seleção for alterada (onSelectionChange).

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

Em seguida, adicione o seguinte processador imediatamente antes do // BOOKMARK 2 comentário:

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

Agora, quando um item na lista estiver selecionado, verá que o botão Transferir já não está desativado.

Captura de ecrã do botão Transferir ativado quando um item está selecionado.

A opção de transferência utilizará uma hiperligação oculta que iremos definir programaticamente primeiro a ligação de transferência para o item selecionado e, em seguida, efetuar o seguinte programaticamente:

  1. Defina o URL da hiperligação para o URL de transferência do item.
  2. Selecione a hiperligação.

Isto irá acionar a transferência para o utilizador.

Adicione a seguinte marcação logo após a abertura <div> no return() método :

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

Agora, localize a constante existente columns que adicionou anteriormente e localize a createTableColumn que referencia o columnId: 'actions'. renderCell Na propriedade , adicione um onClick processador que irá chamar o onDownloadItemClick. Quando terminar, o botão deverá ter o seguinte aspeto:

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

Por fim, adicione o seguinte processador imediatamente após o processador de eventos existente onSelectionChange que adicionou anteriormente. Isto irá processar estes dois passos programáticos mencionados anteriormente:

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

Guarde as alterações, atualize o browser e selecione a ligação Transferir para ver o ficheiro ser transferido.

Captura de ecrã do novo suporte para transferir o ficheiro.

Adicionar a capacidade de criar uma pasta num Contentor

Vamos continuar a criar o <Files /> componente ao adicionar suporte para criar e apresentar pastas.

Comece por adicionar o seguinte código imediatamente antes do // BOOKMARK 1 comentário. Isto irá adicionar os valores de estado do React necessários que iremos utilizar:

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

Para criar uma nova pasta, iremos apresentar uma caixa de diálogo ao utilizador quando selecionarem um botão na barra de ferramentas.

return() No método , imediatamente antes do <DataGrid>, adicione o seguinte código para implementar a caixa de diálogo:

<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>

A caixa de diálogo utiliza alguns estilos personalizados que ainda não adicionámos. Faça-o agora ao adicionar primeiro o seguinte código imediatamente antes da declaração do Files componente:

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

Em seguida, adicione o seguinte código imediatamente antes do return() método no nosso componente:

const styles = useStyles();

Agora que a IU está configurada, precisamos agora de adicionar alguns processadores. Adicione os seguintes processadores imediatamente antes do // BOOKMARK 2 comentário. Estes irão processar a abertura da caixa de diálogo, guardar o valor do nome da nova pasta e o que acontece quando selecionam o botão na caixa de diálogo:

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);
};

Guarde as alterações, atualize o browser e selecione o botão Nova Pasta acima do conteúdo do Contentor:

Captura de ecrã a mostrar o botão Nova Pasta.

Selecione o botão Nova Pasta , introduza um nome e selecione o botão Criar Pasta na caixa de diálogo.

Captura de ecrã a mostrar a caixa de diálogo Criar Nova Pasta.

Quando a pasta for criada, irá vê-la listada na tabela de conteúdos:

Captura de ecrã da nova pasta no Contentor.

Precisamos de fazer mais uma alteração ao nosso componente. Neste momento, quando seleciona uma pasta, esta inicia o URL num novo separador, deixando de fora a aplicação. Não é o que queremos... queremos que desagregar a pasta.

Vamos corrigir isso ao localizar a constante existente columns que adicionou anteriormente e encontrar o createTableColumn que referencia o columnId: 'driveItemName'. renderCell Na propriedade , substitua o componente existente <Link /> pelo seguinte código. Esta ação irá gerar duas ligações com base em se o item atual que está a ser composto for uma pasta ou um ficheiro:

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

Agora, quando seleciona uma pasta, a aplicação irá mostrar o conteúdo da pasta.

Adicionar capacidade para eliminar um ficheiro ou pasta

O passo seguinte consiste em adicionar a capacidade de eliminar uma pasta ou ficheiro do Contentor.

Para tal, comece por adicionar o seguinte código à lista de useState() chamadas existente antes do // BOOKMARK 1 comentário

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

Em seguida, adicione uma caixa de diálogo para atuar como uma confirmação quando o utilizador selecionar o botão Eliminar . Adicione o seguinte código logo após o componente existente <Dialog> no return() método :

<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>

Atualize o botão Eliminar para fazer algo quando estiver selecionado. Localize a constante existente columns que adicionou anteriormente e localize a createTableColumn que referencia o columnId: 'actions'. renderCell Na propriedade , adicione um onClick processador que irá chamar o onDeleteDialogOpen. Quando terminar, o botão deverá ter o seguinte aspeto:

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

Por fim, adicione o seguinte código imediatamente antes do // BOOKMARK 2 comentário para processar a eliminação do item atualmente selecionado:

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);
}

Guarde as alterações e atualize o browser. Selecione o botão Eliminar numa das pastas ou ficheiros existentes na sua Coleção. A caixa de diálogo de confirmação será apresentada e, quando selecionar o botão Eliminar na caixa de diálogo, a tabela de conteúdos do Contentor será atualizada para mostrar que o item foi eliminado:

Captura de ecrã a mostrar a funcionalidade de eliminação de itens num Contentor.

Adicionar a capacidade de carregar ficheiros para o Contentor

O último passo é adicionar a capacidade de carregar ficheiros para um Contentor ou uma pasta num Contentor.

Comece por adicionar o seguinte código imediatamente antes do // BOOKMARK 1 comentário:

const uploadFileRef = useRef<HTMLInputElement>(null);

Em seguida, vamos repetir uma técnica semelhante com um controlo oculto <Input> para carregar um ficheiro. Adicione o seguinte código imediatamente após a abertura <div> no método do return() componente:

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

Adicione um botão à barra de ferramentas para acionar a caixa de diálogo de seleção de ficheiros. Para tal, adicione o seguinte código imediatamente após o botão da barra de ferramentas existente que adiciona uma nova pasta:

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

Por fim, adicione o seguinte código imediatamente antes do // BOOKMARK 2 comentário para adicionar dois processadores de eventos. O onUploadFileClick processador é acionado quando seleciona o botão da barra de ferramentas Carregar Ficheiro e o processador é acionado onUploadFileSelected quando o utilizador seleciona um ficheiro:

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}`);
  });
};

Teste as alterações guardando o ficheiro, atualizando o browser e selecionando o botão Carregar Ficheiro :

Captura de ecrã a mostrar o botão Carregar Ficheiro.

Depois de selecionar um ficheiro, a nossa aplicação irá carregar o ficheiro e atualizar o índice do Contentor:

Captura de ecrã do novo ficheiro apresentado no Contentor.

Resumo

Neste exercício, atualizou o projeto existente para armazenar e gerir ficheiros num Contentor Do SharePoint Embedded.

Verifique seu conhecimento

1.

O que são os Contentores Do SharePoint Embedded implementados como no Microsoft Graph?

2.

Como pode criar um ficheiro num Contentor Incorporado do SharePoint com o Microsoft Graph?

3.

Como eliminar um DriveItem num Contentor Do SharePoint Embedded com o Microsoft Graph?