Creare, aggiornare ed eliminare file all'interno di un contenitore
- 11 minuti
In questo esercizio si aggiornerà il progetto esistente per archiviare i file in un contenitore incorporato di SharePoint.
Aggiungere dichiarazioni di tipo Microsoft Graph al progetto
Prima di creare i nuovi componenti React, si inizierà aggiornando il progetto.
In questo esercizio verranno usati i tipi forniti da Microsoft Graph. Poiché non è stato installato il pacchetto npm che li include, è necessario eseguire prima questa operazione.
Dalla riga di comando eseguire il comando seguente dalla cartella radice del progetto:
npm install @microsoft/microsoft-graph-types -DE
Aggiornare il componente contenitori React per visualizzare i file
Richiamo dell'esercizio precedente, nel componente Contenitori è stato lasciato un segnaposto che verrà usato per visualizzare il contenuto del contenitore selezionato.
Non è stato creato il Files componente, ma si inizierà aggiornando il Containers componente per sostituire il segnaposto con il Files componente che verrà creato.
Individuare e aprire il file ./src/components/containers.tsx .
Aggiungere l'istruzione import seguente all'elenco delle importazioni esistenti nella parte superiore del file:
import { Files } from "./files";
Individuare quindi il codice segnaposto seguente alla fine del file...
{selectedContainer && (`[[TOOD]] container "${selectedContainer.displayName}" contents go here`)}
… e sostituirlo con il codice seguente:
{selectedContainer && (<Files container={selectedContainer} />)}
Creare il componente file React
Si inizierà creando un nuovo componente React per visualizzare e gestire il contenuto dei contenitori.
Creare un nuovo file, ./src/components/files.tsx, e aggiungerlo al codice seguente. Si tratta del componente boilerplate che include tutte le importazioni e lo scheletro del 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
Si notino i // BOOKMARK # commenti nel componente. Questi elementi garantiscono l'aggiunta di codice nelle posizioni corrette.
Visualizzare un elenco del contenuto del contenitore selezionato
La prima cosa da risolvere è visualizzare il contenuto del contenitore selezionato. A tale scopo, verrà usato il DataGrid componente della libreria React fluent UI.
Aggiungere il markup seguente all'interno dell'elemento <div> nell'istruzione return() dopo il // BOOKMARK 3 commento:
<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>
contiene DataGrid alcuni riferimenti a raccolte, impostazioni e metodi che è necessario configurare. Si inizierà con gli elementi visivi, quindi si otterranno i dati.
Le colonne in DataGrid possono essere ridimensionate in base alle proprietà impostate. Creare una nuova costante, columnSizingOptionse aggiungere il codice immediatamente prima del // BOOKMARK 3 commento:
const columnSizingOptions = {
driveItemName: {
minWidth: 150,
defaultWidth: 250,
idealWidth: 200
},
lastModifiedTimestamp: {
minWidth: 150,
defaultWidth: 150
},
lastModifiedBy: {
minWidth: 150,
defaultWidth: 150
},
actions: {
minWidth: 250,
defaultWidth: 250
}
};
Definire quindi la struttura e le impostazioni di rendering per tutte le colonne in DataGrid. A tale scopo, creare una nuova raccolta, columnse aggiungerla immediatamente prima dell'oggetto columnSizingOptions creato:
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>
</>
)
}
}),
];
Questo codice userà il metodo createTableColumn() di utilità per assegnare a ogni colonna un ID e specificare come viene eseguito il rendering delle celle dell'intestazione e del corpo nella tabella.
Con l'oggetto DataGrid configurato, aggiungere le costanti seguenti per gestire lo stato dell'app React con le proprietà che il codice esistente usa. Aggiungere il codice seguente immediatamente prima del commento // BOOKMARK 1:
const [driveItems, setDriveItems] = useState<IDriveItemExtended[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<SelectionItemId>>(new Set<TableRowId>([1]));
Aggiungere ora alcuni gestori per recuperare e visualizzare i dati dal contenitore.
Aggiungere il gestore seguente e React hook per ottenere il contenuto del contenitore selezionato. L'hook useEffect verrà eseguito la prima volta che viene eseguito il rendering del componente e quando cambiano le <Files />proprietà di input del componente.
Aggiungere il codice seguente immediatamente prima del commento // 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 funzione usa il client Microsoft Graph per ottenere un elenco di tutti i file all'interno della cartella corrente, per impostazione predefinita root se non è già selezionata alcuna cartella.
Accetta quindi la raccolta di DriveItems restituita da Microsoft Graph e aggiunge altre proprietà per semplificare il codice in un secondo momento. Alla fine del metodo chiama il metodo della setDriveitems() funzione di accesso allo stato che attiverà un nuovo rendering del componente. L'oggetto driveItems viene impostato sulla DataGrid.items proprietà che spiega il motivo per cui nella tabella vengono visualizzate alcune informazioni.
Testare il rendering dell'elenco dei contenuti di un contenitore
Ora testiamo l'app React lato client per verificare che il <Files /> componente visualizzi il contenuto del contenitore selezionato.
Dalla riga di comando nella cartella radice del progetto eseguire il comando seguente:
npm run start
Al caricamento del browser, accedere usando lo stesso account aziendale e dell'istituto di istruzione in uso.
Dopo l'accesso, selezionare un contenitore esistente. Se il contenitore contiene già contenuto, verrà visualizzato come segue:
Se si seleziona il file, in questo caso un documento Word, verrà aperta una nuova scheda e verrà caricato l'URL dell'elemento. Per questo esempio, il file viene aperto in Word online.
Aggiungere il supporto per il download di file
Al termine della funzionalità di visualizzazione del contenuto, aggiorneremo il componente per supportare il download dei file.
Per iniziare, aggiungere il codice seguente immediatamente prima del // BOOKMARK 1 commento:
const downloadLinkRef = useRef<HTMLAnchorElement>(null);
Successivamente, si vuole assicurarsi che un elemento in DataGrid sia selezionato prima che possa scaricarlo. In caso contrario, il pulsante Scarica verrà disabilitato così come è attualmente.
DataGridIn aggiungere tre proprietà per impostarla in modo da supportare una modalità di selezione di un singolo elemento (selectionMode), tenere traccia degli elementi selezionati (selectedItems) e delle operazioni da eseguire quando la selezione cambia (onSelectionChange).
<DataGrid
...
selectionMode='single'
selectedItems={selectedRows}
onSelectionChange={onSelectionChange}>
Aggiungere quindi il gestore seguente immediatamente prima del // BOOKMARK 2 commento:
const onSelectionChange: DataGridProps["onSelectionChange"] = (event: React.MouseEvent | React.KeyboardEvent, data: OnSelectionChangeData): void => {
setSelectedRows(data.selectedItems);
}
Ora che viene selezionato un elemento nell'elenco, verrà visualizzato che il pulsante Scarica non è più disabilitato.
L'opzione di download userà un collegamento ipertestuale nascosto che verrà prima impostato a livello di codice per l'elemento selezionato e quindi a livello di codice:
- Impostare l'URL del collegamento ipertestuale sull'URL di download dell'elemento.
- Selezionare il collegamento ipertestuale.
Verrà attivato il download per l'utente.
Aggiungere il markup seguente subito dopo l'apertura <div> nel return() metodo :
<a ref={downloadLinkRef} href="" target="_blank" style={{ display: 'none' }} />
Individuare ora la costante esistente columns aggiunta in precedenza e trovare l'oggetto createTableColumn che fa riferimento a columnId: 'actions'.
renderCell Nella proprietà aggiungere un onClick gestore che chiamerà l'oggetto onDownloadItemClick. Al termine, il pulsante dovrebbe essere simile al seguente:
<Button aria-label="Download"
disabled={!selectedRows.has(driveItem.id as string)}
icon={<SaveRegular />}
onClick={() => onDownloadItemClick(driveItem.downloadUrl)}>Download</Button>
Aggiungere infine il gestore seguente immediatamente dopo il gestore eventi esistente onSelectionChange aggiunto in precedenza. Verranno gestiti i due passaggi a livello di codice indicati in precedenza:
const onDownloadItemClick = (downloadUrl: string) => {
const link = downloadLinkRef.current;
link!.href = downloadUrl;
link!.click();
}
Salvare le modifiche, aggiornare il browser e selezionare il collegamento Scarica per visualizzare il file scaricato.
Aggiungere la possibilità di creare una cartella in un contenitore
Continuare a compilare il <Files /> componente aggiungendo il supporto per la creazione e la visualizzazione di cartelle.
Per iniziare, aggiungere il codice seguente immediatamente prima del // BOOKMARK 1 commento. Verranno aggiunti i valori di stato React necessari che verranno usati:
const [folderId, setFolderId] = useState<string>('root');
const [folderName, setFolderName] = useState<string>('');
const [creatingFolder, setCreatingFolder] = useState<boolean>(false);
const [newFolderDialogOpen, setNewFolderDialogOpen] = useState(false);
Per creare una nuova cartella, verrà visualizzata una finestra di dialogo per l'utente quando seleziona un pulsante sulla barra degli strumenti.
All'interno del return() metodo , immediatamente prima di <DataGrid>, aggiungere il codice seguente per implementare la finestra di dialogo:
<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 finestra di dialogo usa alcuni stili personalizzati che non sono ancora stati aggiunti. Eseguire questa operazione aggiungendo prima di tutto il codice seguente immediatamente prima della dichiarazione del Files componente:
const useStyles = makeStyles({
dialogInputControl: {
width: '400px',
},
dialogContent: {
display: 'flex',
flexDirection: 'column',
rowGap: '10px',
marginBottom: '25px'
}
});
Aggiungere quindi il codice seguente immediatamente prima del return() metodo nel componente:
const styles = useStyles();
Ora che l'interfaccia utente è stata configurata, è ora necessario aggiungere alcuni gestori. Aggiungere i gestori seguenti immediatamente prima del // BOOKMARK 2 commento. Questi gestiranno l'apertura della finestra di dialogo, salvando il valore del nome della nuova cartella e cosa accade quando selezionano il pulsante nella finestra di dialogo:
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);
};
Salvare le modifiche, aggiornare il browser e selezionare il pulsante Nuova cartella sopra il contenuto del contenitore:
Selezionare il pulsante Nuova cartella , immettere un nome e selezionare il pulsante Crea cartella nella finestra di dialogo.
Quando la cartella viene creata, verrà visualizzata nell'elenco dei contenuti:
È necessario apportare un'altra modifica al componente. Al momento, quando si seleziona una cartella, verrà avviato l'URL in una nuova scheda che esce dall'app. Non è quello che vogliamo... si vuole eseguire il drill-in nella cartella.
Si correggi questo problema individuando la costante esistente columns aggiunta in precedenza e trovando l'oggetto createTableColumn che fa riferimento a columnId: 'driveItemName'.
renderCell Nella proprietà sostituire il componente esistente <Link /> con il codice seguente. Verranno generati due collegamenti in base a se l'elemento corrente di cui viene eseguito il rendering è una cartella o un file:
{(!driveItem.isFolder)
? <Link href={driveItem!.webUrl!} target='_blank'>{driveItem.name}</Link>
: <Link onClick={() => {
loadItems(driveItem.id);
setFolderId(driveItem.id as string)
}}>{driveItem.name}</Link>
}
Ora, quando si seleziona una cartella, l'app mostrerà il contenuto della cartella.
Aggiungere la possibilità di eliminare un file o una cartella
Il passaggio successivo consiste nell'aggiungere la possibilità di eliminare una cartella o un file dal contenitore.
A tale scopo, iniziare aggiungendo il codice seguente all'elenco esistente di useState() chiamate prima del // BOOKMARK 1 commento
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
Aggiungere quindi una finestra di dialogo che funge da conferma quando l'utente seleziona il pulsante Elimina . Aggiungere il codice seguente subito dopo il componente esistente <Dialog> nel return() metodo :
<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>
Aggiornare il pulsante Elimina per eseguire un'operazione quando è selezionata. Individuare la costante esistente columns aggiunta in precedenza e trovare l'oggetto createTableColumn che fa riferimento a columnId: 'actions'.
renderCell Nella proprietà aggiungere un onClick gestore che chiamerà l'oggetto onDeleteDialogOpen. Al termine, il pulsante dovrebbe essere simile al seguente:
<Button aria-label="Delete"
icon={<DeleteRegular />}
onClick={() => setDeleteDialogOpen(true)}>Delete</Button>
Aggiungere infine il codice seguente immediatamente prima del // BOOKMARK 2 commento per gestire l'eliminazione dell'elemento attualmente selezionato:
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);
}
Salvare le modifiche e aggiornare il browser. Selezionare il pulsante Elimina in una delle cartelle o dei file esistenti nella raccolta. Verrà visualizzata la finestra di dialogo di conferma e quando si seleziona il pulsante Elimina nella finestra di dialogo, la tabella dei contenuti del contenitore verrà aggiornata per mostrare che l'elemento è stato eliminato:
Aggiungere la possibilità di caricare file nel contenitore
L'ultimo passaggio consiste nell'aggiungere la possibilità di caricare file in un contenitore o in una cartella all'interno di un contenitore.
Per iniziare, aggiungere il codice seguente immediatamente prima del // BOOKMARK 1 commento:
const uploadFileRef = useRef<HTMLInputElement>(null);
Successivamente, si ripeterà una tecnica simile usando un controllo nascosto <Input> per caricare un file. Aggiungere il codice seguente immediatamente dopo l'apertura <div> nel metodo del return() componente:
<input ref={uploadFileRef} type="file" onChange={onUploadFileSelected} style={{ display: 'none' }} />
Aggiungere un pulsante alla barra degli strumenti per attivare la finestra di dialogo di selezione del file. A tale scopo, aggiungere il codice seguente immediatamente dopo il pulsante della barra degli strumenti esistente che aggiunge una nuova cartella:
<ToolbarButton vertical icon={<ArrowUploadRegular />} onClick={onUploadFileClick}>Upload File</ToolbarButton>
Aggiungere infine il codice seguente immediatamente prima del // BOOKMARK 2 commento per aggiungere due gestori eventi. Il onUploadFileClick gestore viene attivato quando si seleziona il pulsante della barra degli strumenti Carica file e il onUploadFileSelected gestore viene attivato quando l'utente seleziona un file:
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}`);
});
};
Testare le modifiche salvando il file, aggiornando il browser e selezionando il pulsante Carica file :
Dopo aver selezionato un file, l'app caricherà il file e aggiornerà la tabella del contenuto del contenitore:
Riepilogo
In questo esercizio è stato aggiornato il progetto esistente per archiviare e gestire i file in un contenitore incorporato di SharePoint.