Creación, actualización y eliminación de archivos dentro de un contenedor
En este ejercicio, actualizará el proyecto existente para almacenar archivos en un contenedor incrustado de SharePoint.
Agregar declaraciones de tipos de Microsoft Graph al proyecto
Antes de crear los nuevos componentes de React, comencemos por actualizar el proyecto.
A lo largo de este ejercicio, usaremos los tipos proporcionados por Microsoft Graph. Dado que no hemos instalado el paquete npm que los incluye, primero debemos hacerlo.
Desde la línea de comandos, ejecute el siguiente comando desde la carpeta raíz del proyecto:
npm install @microsoft/microsoft-graph-types -DE
Actualizar el componente Contenedores de React para mostrar archivos
Al recuperar del ejercicio anterior, dejamos un marcador de posición en el componente Contenedores que se usará para mostrar el contenido del contenedor seleccionado.
No hemos creado el Files
componente, pero vamos a empezar actualizando el Containers
componente para reemplazar el marcador de posición por el Files
componente que vamos a crear.
Busque y abra el archivo ./src/components/containers.tsx .
Agregue la siguiente instrucción import a la lista de importaciones existentes en la parte superior del archivo:
import { Files } from "./files";
A continuación, busque el siguiente código de marcador de posición cerca del final del archivo...
{selectedContainer && (`[[TOOD]] container "${selectedContainer.displayName}" contents go here`)}
… y reemplácela por el código siguiente:
{selectedContainer && (<Files container={selectedContainer} />)}
Creación del componente Files React
Comencemos por crear un nuevo componente de React para mostrar y administrar el contenido de los contenedores.
Cree un nuevo archivo, ./src/components/files.tsx, y agregue el código siguiente. Este es el componente reutilizable que incluye todas las importaciones y el esqueleto de nuestro 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;
Nota:
Observe los // BOOKMARK #
comentarios del componente. Esto para asegurarse de que va a agregar código en los lugares correctos.
Mostrar una lista del contenido del contenedor seleccionado
Lo primero que debemos abordar es mostrar el contenido del contenedor seleccionado. Para ello, usaremos el componente de la DataGrid
biblioteca Fluent UI React.
Agregue el siguiente marcado dentro del <div>
elemento en la return()
instrucción después del // BOOKMARK 3
comentario:
<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>
DataGrid
contiene algunas referencias a colecciones, configuraciones y métodos que necesitamos configurar. Comencemos con los elementos visuales y, a continuación, se obtendrán los datos.
Las columnas de DataGrid
se pueden cambiar de tamaño según las propiedades que establezcamos. Cree una nueva constante, columnSizingOptions
y agregue el código inmediatamente antes del // BOOKMARK 3
comentario:
const columnSizingOptions = {
driveItemName: {
minWidth: 150,
defaultWidth: 250,
idealWidth: 200
},
lastModifiedTimestamp: {
minWidth: 150,
defaultWidth: 150
},
lastModifiedBy: {
minWidth: 150,
defaultWidth: 150
},
actions: {
minWidth: 250,
defaultWidth: 250
}
};
A continuación, defina la estructura y la configuración de representación de todas las columnas de DataGrid
. Para ello, cree una nueva colección, columns
y agréguela inmediatamente antes de crear columnSizingOptions
:
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 usará el método createTableColumn()
de utilidad para proporcionar un identificador a cada columna y especificar cómo se representan las celdas de encabezado y cuerpo de la tabla.
Con configurado DataGrid
, agregue las siguientes constantes para administrar el estado de la aplicación React con las propiedades que usa nuestro código existente. Agregue el código siguiente inmediatamente antes del comentario // BOOKMARK 1
:
const [driveItems, setDriveItems] = useState<IDriveItemExtended[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<SelectionItemId>>(new Set<TableRowId>([1]));
Ahora, vamos a agregar algunos controladores para capturar y mostrar los datos de nuestro contenedor.
Agregue el siguiente controlador y el enlace de React para obtener el contenido del contenedor seleccionado. El useEffect
enlace se ejecutará la primera vez que se represente el componente, así como cuando cambien las <Files />
propiedades de entrada del componente.
Agregue el código siguiente inmediatamente antes del comentario // 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
función usa el cliente de Microsoft Graph para obtener una lista de todos los archivos de la carpeta actual, de root
forma predeterminada si no hay ninguna carpeta seleccionada.
A continuación, toma la colección de DriveItems
devuelta por Microsoft Graph y agrega algunas propiedades más para simplificar el código más adelante. Al final del método, llama al setDriveitems()
método de descriptor de acceso de estado que desencadenará una nueva representación del componente.
driveItems
se establecen en la DataGrid.items
propiedad que explica por qué la tabla muestra cierta información.
Prueba de la representación de la enumeración del contenido de un contenedor
Ahora vamos a probar la aplicación React del lado cliente para asegurarse de que el <Files />
componente muestra el contenido del contenedor seleccionado.
Desde la línea de comandos de la carpeta raíz del proyecto, ejecute el siguiente comando:
npm run start
Cuando se cargue el explorador, inicie sesión con la misma cuenta profesional y educativa que ha estado usando.
Después de iniciar sesión, seleccione un contenedor existente. Si ese contenedor ya tiene contenido, se mostrará de la siguiente manera:
Si selecciona el archivo, en este caso un documento de Word, abrirá una nueva pestaña y cargará la dirección URL del elemento. En este ejemplo, el archivo se abre en Word online.
Agregar compatibilidad para descargar archivos
Una vez completada la característica de visualización de contenido, vamos a actualizar el componente para admitir la descarga de archivos.
Para empezar, agregue el código siguiente inmediatamente antes del // BOOKMARK 1
comentario:
const downloadLinkRef = useRef<HTMLAnchorElement>(null);
A continuación, queremos asegurarnos de que se selecciona un elemento de DataGrid
antes de que puedan descargarlo. De lo contrario, el botón Descargar se deshabilitará tal y como está actualmente.
En , DataGrid
agregue tres propiedades para establecerla para que admita un modo de selección de elementos único (selectionMode
), realice un seguimiento de qué elementos están seleccionados (selectedItems
) y qué hacer cuando cambie la selección (onSelectionChange
).
<DataGrid
...
selectionMode='single'
selectedItems={selectedRows}
onSelectionChange={onSelectionChange}>
A continuación, agregue el siguiente controlador inmediatamente antes del // BOOKMARK 2
comentario:
const onSelectionChange: DataGridProps["onSelectionChange"] = (event: React.MouseEvent | React.KeyboardEvent, data: OnSelectionChangeData): void => {
setSelectedRows(data.selectedItems);
}
Ahora, cuando se selecciona un elemento de la lista, verá que el botón Descargar ya no está deshabilitado.
La opción de descarga usará un hipervínculo oculto que primero estableceremos mediante programación el vínculo de descarga para el elemento seleccionado y, a continuación, realizaremos mediante programación lo siguiente:
- Establezca la dirección URL del hipervínculo en la dirección URL de descarga del elemento.
- Seleccione el hipervínculo.
Esto desencadenará la descarga del usuario.
Agregue el marcado siguiente justo después de la apertura <div>
en el return()
método :
<a ref={downloadLinkRef} href="" target="_blank" style={{ display: 'none' }} />
Ahora, busque la constante existente columns
que agregó anteriormente y busque que createTableColumn
hace referencia a columnId: 'actions'
. En la renderCell
propiedad , agregue un onClick
controlador que llamará a onDownloadItemClick
. El botón debe tener un aspecto similar al siguiente cuando haya terminado:
<Button aria-label="Download"
disabled={!selectedRows.has(driveItem.id as string)}
icon={<SaveRegular />}
onClick={() => onDownloadItemClick(driveItem.downloadUrl)}>Download</Button>
Por último, agregue el siguiente controlador inmediatamente después del controlador de eventos existente onSelectionChange
que agregó anteriormente. Esto controlará los dos pasos de programación mencionados anteriormente:
const onDownloadItemClick = (downloadUrl: string) => {
const link = downloadLinkRef.current;
link!.href = downloadUrl;
link!.click();
}
Guarde los cambios, actualice el explorador y seleccione el vínculo Descargar para ver cómo se descarga el archivo.
Agregar la capacidad de crear una carpeta en un contenedor
Vamos a seguir creando el <Files />
componente agregando compatibilidad para crear y mostrar carpetas.
Empiece agregando el código siguiente inmediatamente antes del // BOOKMARK 1
comentario. Esto agregará los valores de estado de React necesarios que usaremos:
const [folderId, setFolderId] = useState<string>('root');
const [folderName, setFolderName] = useState<string>('');
const [creatingFolder, setCreatingFolder] = useState<boolean>(false);
const [newFolderDialogOpen, setNewFolderDialogOpen] = useState(false);
Para crear una nueva carpeta, se mostrará un cuadro de diálogo al usuario cuando seleccione un botón en la barra de herramientas.
En el return()
método , inmediatamente antes de <DataGrid>
, agregue el código siguiente para implementar el cuadro 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>
El cuadro de diálogo usa algunos estilos personalizados que aún no hemos agregado. Para ello, agregue primero el código siguiente inmediatamente antes de la declaración del Files
componente:
const useStyles = makeStyles({
dialogInputControl: {
width: '400px',
},
dialogContent: {
display: 'flex',
flexDirection: 'column',
rowGap: '10px',
marginBottom: '25px'
}
});
A continuación, agregue el código siguiente inmediatamente antes del return()
método en nuestro componente:
const styles = useStyles();
Ahora que la interfaz de usuario está configurada, ahora es necesario agregar algunos controladores. Agregue los siguientes controladores inmediatamente antes del // BOOKMARK 2
comentario. Estos controlarán la apertura del cuadro de diálogo, guardando el valor del nombre de la nueva carpeta y lo que sucede cuando seleccionan el botón en el cuadro 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 los cambios, actualice el explorador y seleccione el botón Nueva carpeta encima del contenido del contenedor:
Seleccione el botón Nueva carpeta , escriba un nombre y seleccione el botón Crear carpeta en el cuadro de diálogo.
Cuando se cree la carpeta, verá que aparece en la tabla de contenido:
Tenemos que realizar un cambio más en nuestro componente. En este momento, al seleccionar una carpeta, se iniciará la dirección URL en una nueva pestaña que deja fuera la aplicación. Eso no es lo que queremos... queremos que se profundice en la carpeta.
Vamos a corregirlo localizando la constante existente columns
que agregó anteriormente y busque la createTableColumn
que hace referencia a columnId: 'driveItemName'
. En la renderCell
propiedad , reemplace el componente existente <Link />
por el código siguiente. Esto generará dos vínculos en función de si el elemento actual que se representa es una carpeta o un archivo:
{(!driveItem.isFolder)
? <Link href={driveItem!.webUrl!} target='_blank'>{driveItem.name}</Link>
: <Link onClick={() => {
loadItems(driveItem.id);
setFolderId(driveItem.id as string)
}}>{driveItem.name}</Link>
}
Ahora, al seleccionar una carpeta, la aplicación mostrará el contenido de la carpeta.
Adición de la capacidad de eliminar un archivo o carpeta
El siguiente paso es agregar la capacidad de eliminar una carpeta o un archivo del contenedor.
Para ello, empiece agregando el código siguiente a la lista existente de useState()
llamadas antes del // BOOKMARK 1
comentario.
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
A continuación, agregue un cuadro de diálogo para que actúe como confirmación cuando el usuario seleccione el botón Eliminar . Agregue el código siguiente justo después del componente existente <Dialog>
en el 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>
Actualice el botón Eliminar para hacer algo cuando esté seleccionado. Busque la constante existente columns
que agregó anteriormente y busque la createTableColumn
que hace referencia a columnId: 'actions'
. En la renderCell
propiedad , agregue un onClick
controlador que llamará a onDeleteDialogOpen
. El botón debe tener un aspecto similar al siguiente cuando haya terminado:
<Button aria-label="Delete"
icon={<DeleteRegular />}
onClick={() => setDeleteDialogOpen(true)}>Delete</Button>
Por último, agregue el código siguiente inmediatamente antes del // BOOKMARK 2
comentario para controlar la eliminación del elemento seleccionado actualmente:
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 los cambios y actualice el explorador. Seleccione el botón Eliminar en una de las carpetas o archivos existentes de la colección. Aparecerá el cuadro de diálogo de confirmación y, al seleccionar el botón Eliminar del cuadro de diálogo, se actualizará la tabla de contenido del contenedor para mostrar que el elemento se ha eliminado:
Agregar la capacidad de cargar archivos en el contenedor
El último paso es agregar la capacidad de cargar archivos en un contenedor o una carpeta dentro de un contenedor.
Para empezar, agregue el código siguiente inmediatamente antes del // BOOKMARK 1
comentario:
const uploadFileRef = useRef<HTMLInputElement>(null);
A continuación, repetiremos una técnica similar mediante un control oculto <Input>
para cargar un archivo. Agregue el código siguiente inmediatamente después de la apertura <div>
en el método del return()
componente:
<input ref={uploadFileRef} type="file" onChange={onUploadFileSelected} style={{ display: 'none' }} />
Agregue un botón a la barra de herramientas para desencadenar el cuadro de diálogo de selección de archivos. Para ello, agregue el código siguiente inmediatamente después del botón de barra de herramientas existente que agrega una nueva carpeta:
<ToolbarButton vertical icon={<ArrowUploadRegular />} onClick={onUploadFileClick}>Upload File</ToolbarButton>
Por último, agregue el código siguiente inmediatamente antes del // BOOKMARK 2
comentario para agregar dos controladores de eventos. El onUploadFileClick
controlador se desencadena al seleccionar el botón de barra de herramientas Cargar archivo y el onUploadFileSelected
controlador se desencadena cuando el usuario selecciona un archivo:
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}`);
});
};
Pruebe los cambios guardando el archivo, actualizando el explorador y seleccionando el botón Cargar archivo :
Después de seleccionar un archivo, nuestra aplicación cargará el archivo y actualizará la tabla del contenido del contenedor:
Resumen
En este ejercicio, ha actualizado el proyecto existente para almacenar y administrar archivos en un contenedor incrustado de SharePoint.