|
| 1 | +# @pnp/graph/workbooks |
| 2 | + |
| 3 | +Provides the ability to interact with Excel workbooks hosted in a Drive. |
| 4 | + |
| 5 | +More information can be found on the official Graph documentation: |
| 6 | + |
| 7 | +- [Workbooks and charts](https://learn.microsoft.com/en-us/graph/api/resources/excel) |
| 8 | + |
| 9 | +## Opening a workbook |
| 10 | +To open an Excel workbook, create an [IDriveItem](./files.md) pointing to an .xlsx file with `DriveItem.getItemByID`. Then, use `getWorkbookSession(persistChanges)` to open the workbook. |
| 11 | + |
| 12 | +Use the persistChanges parameter to set whether you want your changes to be saved back to the file. |
| 13 | + |
| 14 | +```Typescript |
| 15 | +import { PreferAsync } from "@pnp/graph/behaviors/prefer-async.js"; |
| 16 | +import "@pnp/graph/files/index.js"; |
| 17 | +import "@pnp/graph/workbooks/index.js"; |
| 18 | + |
| 19 | +const drive = graph.me.drive(); |
| 20 | + |
| 21 | +const { id: fileId } = await drive |
| 22 | + .getItemByPath('path/to/MyWorkbook.xlsx') |
| 23 | + .select('id')(); |
| 24 | + |
| 25 | +const workbook = await drive.getItemById(fileId) |
| 26 | + .using(PreferAsync()) |
| 27 | + .getWorkbookSession(false); |
| 28 | + |
| 29 | +// Do stuff... |
| 30 | + |
| 31 | +await workbook.closeSession(); |
| 32 | +``` |
| 33 | +**KNOWN BUG**: You MUST open the workbook on a DriveItem that was located by ID. Calling `getWorkbookSession` on a DriveItem located by path will fail with "AccessDenied: Could not obtain a WAC access token." |
| 34 | + |
| 35 | +Using `PreferAsync()` is not required. However, some of the workbook endpoints support the [long-running operation pattern](https://learn.microsoft.com/en-us/graph/workbook-best-practice?tabs=http#work-with-apis-that-take-a-long-time-to-complete), so using the PreferAsync behaviour may make your life easier. |
| 36 | + |
| 37 | +## Working with named tables |
| 38 | +### Reading values |
| 39 | +```Typescript |
| 40 | +const table = workbook.tables.getByName("MyTable1"); |
| 41 | + |
| 42 | +// Column names |
| 43 | +const { values: columnNames } = await table.headerRowRange.select("values")(); |
| 44 | + |
| 45 | +// All data rows and columns |
| 46 | +const { values: tableRows } = await table.dataBodyRange.select("values")(); |
| 47 | + |
| 48 | +// All rows from the first column (including the header) |
| 49 | +const firstColumn = table.columns.getItemAt(0); |
| 50 | +const { values: rowsFromCol } = await firstColumn.select("values")(); |
| 51 | + |
| 52 | +// Rows 20-30 of the column named "SomeColumn" |
| 53 | +const { values: twenties } = await testTable.columns.getByName("SomeColumn") |
| 54 | + .getRange().cell(19, 0).rowsBelow(10) |
| 55 | + .select("values")(); |
| 56 | + |
| 57 | +// For a large table, use paging to iterate over the rows |
| 58 | +const allRows = []; |
| 59 | +for await (let page of allPages(testTable.rows, 100)) { |
| 60 | + console.info(`Got first page: ${page.values}`) |
| 61 | + allRows.push(...page); |
| 62 | +} |
| 63 | +``` |
| 64 | +See below for a an example implementation of `allPages()`. |
| 65 | + |
| 66 | +#### Async iterate over all pages |
| 67 | +**KNOWN BUG**: Graph workbook endpoints don't currently return the required |
| 68 | +OData properties to work with PnPJS' existing async iterator. |
| 69 | + |
| 70 | +In the meantime, one way to iterate over a whole collection is to simply |
| 71 | +keep requesting pages until there is no more data: |
| 72 | +```Typescript |
| 73 | +export default function allPages<T>(query: IGraphCollection<T>, pageSize: number) { |
| 74 | + return { |
| 75 | + [Symbol.asyncIterator](): AsyncIterator<T> { |
| 76 | + let skipOffset = 0; |
| 77 | + return { |
| 78 | + async next() { |
| 79 | + const response: any = await query.top(pageSize).skip(skipOffset)(); |
| 80 | + if (typeof response.length === 'number' && response.length > 0) { |
| 81 | + skipOffset += response.length; |
| 82 | + return { done: false, value: response } |
| 83 | + } else { |
| 84 | + return { done: true, value: [] } |
| 85 | + } |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | +### Writing values |
| 93 | +```Typescript |
| 94 | +// Appending a row |
| 95 | +const newRow = await table.rows.add({ values: ["a", "b", "c"].map(cell => [cell]) }); |
| 96 | + |
| 97 | +// Deleting a row |
| 98 | +await table.rows.getItemAt(5).delete(); |
| 99 | + |
| 100 | +// Create a new column with no data |
| 101 | +const newEmptyCol = await table.columns.add({ name: "EmptyColumn" }); |
| 102 | + |
| 103 | +``` |
| 104 | +**KNOWN BUG**: If you try to delete a row from a table with a filter currently active, the operation will fail with 409 Conflict and a message stating the operation "won't work" because it would move cells in your table. Possible workarounds are to remove the filter first or use convertToRange to change the table back into a range of regular cells. |
| 105 | +### Updating table properties |
| 106 | +[General properties](https://learn.microsoft.com/en-us/graph/api/resources/workbooktable?view=graph-rest-1.0#properties) can be updated like so: |
| 107 | +```Typescript |
| 108 | +await table.update({ showBandedRows: true }); |
| 109 | +``` |
| 110 | +[Sorting](https://learn.microsoft.com/en-us/graph/api/resources/workbooktablesort?view=graph-rest-1.0) and [filtering](https://learn.microsoft.com/en-us/graph/api/resources/workbookfilter?view=graph-rest-1.0) settings have their own endpoints: |
| 111 | +```Typescript |
| 112 | +// Filter the table to show rows where "MyColumn" is greater than 10 |
| 113 | +const myColumn = table.columns.getByName("MyColumn"); |
| 114 | +await myColumn.filter.apply({ |
| 115 | + criteria: { |
| 116 | + criterion1: '>10', |
| 117 | + filterOn: 'Custom', |
| 118 | + // 'filterOn' is not documented but must be set, otherwise |
| 119 | + // the operation fails with 500. |
| 120 | + // There may be supported values other than 'Custom', but |
| 121 | + // they are not in the Graph API documentation. |
| 122 | + } |
| 123 | + }); |
| 124 | + |
| 125 | +// Sort the table based on the column at index 0 in ascending order |
| 126 | +await table.sort.apply([{ key: 0, ascending: true }]); |
| 127 | +``` |
| 128 | +## Working with ranges |
| 129 | +### Getting a range |
| 130 | +```Typescript |
| 131 | +// Create a range using Excel A1 coordinates |
| 132 | +const sheet = workbook.worksheets.getByID("Sheet1"); |
| 133 | +const range = sheet.getRange("A1:C3"); |
| 134 | + |
| 135 | +// Get the full "used range" of the worksheet |
| 136 | +const usedRange = sheet.getUsedRange(); |
| 137 | +const usedAddress = (await usedRange()).address // = e.g. "B2:L21" |
| 138 | + |
| 139 | +// Named objects (like tables) have an underlying range, too |
| 140 | +const tableRange = table.getRange(); |
| 141 | +``` |
| 142 | +### Modifying values |
| 143 | +```Typescript |
| 144 | +// A single cell in a range |
| 145 | +const sheetRange = sheet.getRange(); |
| 146 | +const cell = sheetRange.cell(0, 1); |
| 147 | +await cell.update({ values: [["Hello, world!"]] }); |
| 148 | + |
| 149 | +// Multiple cells in a range |
| 150 | +const values = [ |
| 151 | + ["a", "b", "c"], |
| 152 | + [1, 2, 3], |
| 153 | + [1, 2, "=SUM(A3:B3)"] |
| 154 | +]; |
| 155 | +await sheet.getRange("A1:C3").update({ values }); |
| 156 | +``` |
| 157 | +### Sorting and formatting |
| 158 | +```Typescript |
| 159 | +// Sort a range in descending order based on its first column |
| 160 | +const sort: WorkbookSortField = { |
| 161 | + key: 0, sortOn: "Value", |
| 162 | + dataOption: "TextAsNumber", |
| 163 | + ascending: false |
| 164 | +}; |
| 165 | + |
| 166 | +await range.sort.apply({ |
| 167 | + fields: [ sort ], hasHeaders: false, |
| 168 | +}); |
| 169 | + |
| 170 | +// Get and set a fill on a range |
| 171 | +const oldFill = await range.format.fill(); |
| 172 | +await range.format.fill.update({ color: "#FF0000" }); |
| 173 | + |
| 174 | +// Add a purple dashed line to the top border of a range |
| 175 | +await range.format.borders.getBySideIndex("EdgeTop").update({ |
| 176 | + color: "#8C34EB", |
| 177 | + style: "Dash", |
| 178 | + weight: "Medium" |
| 179 | +}); |
| 180 | +``` |
| 181 | +## Full example: Creating a table from data |
| 182 | +```Typescript |
| 183 | +const sheet = workbook.worksheets.getById(TEST_SHEET_NAME); |
| 184 | + |
| 185 | +// Add data to the worksheet |
| 186 | +const addr = "A1:C4"; |
| 187 | +const data = [ |
| 188 | + ["Name", "Age", "Department"], |
| 189 | + ["Alice", 30, "Engineering"], |
| 190 | + ["Bob", 25, "HR"], |
| 191 | + ["Charlie", 35, "Finance"] |
| 192 | +]; |
| 193 | + |
| 194 | +const range = sheet.getRange(addr); |
| 195 | +await range.update({ values: data }); |
| 196 | + |
| 197 | +// Convert the range into a named table |
| 198 | +const tableInfo = await sheet.tables.add(addr, true); |
| 199 | +const table = workbook.tables.getById(tableInfo.id!); |
| 200 | + |
| 201 | +// Rename the table and enable banded rows |
| 202 | +await table.update({ |
| 203 | + name: "Staff_list", |
| 204 | + showBandedRows: true |
| 205 | +}); |
| 206 | + |
| 207 | +// Autofit column width |
| 208 | +await table.getRange().format.autofitColumns(); |
| 209 | + |
| 210 | +// Sort the table in ascending order on the "Age" column |
| 211 | +await table.sort.apply([{ key: 1, ascending: true }]); |
| 212 | +``` |
| 213 | +Output: |
| 214 | + |
| 215 | + |
0 commit comments