Skip to content

Commit 4396e13

Browse files
authored
Merge pull request #3213 from pnp/version-4
Release 4.10.0 Merge to Main
2 parents e3290fb + 7715130 commit 4396e13

File tree

20 files changed

+2075
-392
lines changed

20 files changed

+2075
-392
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8-
## 4.9.0 - 2024-Jan-15
8+
## 4.10.0 - 2025-Feb-19
9+
10+
- graph
11+
- Implemented "Workbook" package
12+
- Updates batching logic for sp and graph to remove unnecessary reject carry for the send promise.
13+
14+
- sp
15+
- Adding create change token util method to sp
16+
- Fixed issue when moving list instead of subfolder
17+
- Fix content-type order type on folder
18+
- Adds support for sp batches to auto-split if there are too many items in the batch
19+
- Updates batching logic for sp and graph to remove unnecessary reject carry for the send promise.
20+
21+
## 4.9.0 - 2025-Jan-15
922

1023
- graph
1124
- Added new drive endpoint to lists, which will get you drive information

debug/launch/main.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { ITestingSettings } from "../../test/load-settings.js";
99
// will allow you to keep all your debugging files locally
1010
// comment out the example
1111
import { Example } from "./sp.js";
12-
// import { Example } from "./spadmin.js";
1312
// import { Example } from "./graph.js";
1413

1514
// setup the connection to SharePoint using the settings file, you can

docs/graph/workbooks.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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+
![Table example output](../img/Basic_table_example.png)

docs/img/Basic_table_example.png

15.4 KB
Loading

docs/sp/lists.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,20 +237,18 @@ console.log(r);
237237
To get changes from a specific time range you can use the ChangeTokenStart or a combination of ChangeTokenStart and ChangeTokenEnd.
238238

239239
```TypeScript
240-
import { IChangeQuery } from "@pnp/sp";
240+
import { IChangeQuery, createChangeToken } from "@pnp/sp";
241241

242242
//Resource is the list Id (as Guid)
243-
const resource = list.Id;
244-
const changeStart = new Date("2022-02-22").getTime();
245-
const changeTokenStart = `1;3;${resource};${changeStart};-1`;
243+
const changeTokenStart = createChangeToken("list", list.Id, new Date("2022-02-22"));
246244

247245
// build the changeQuery object, here we look at changes regarding Add and Update for Items.
248246
const changeQuery: IChangeQuery = {
249247
Add: true,
250248
Update: true,
251249
Item: true,
252250
ChangeTokenEnd: null,
253-
ChangeTokenStart: { StringValue: changeTokenStart },
251+
ChangeTokenStart: changeTokenStart,
254252
};
255253

256254
// get list changes

docs/sp/utils.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SP Utilities
2+
3+
## createChangeToken
4+
5+
Helps you create a change token for use with getChanges on Site, Web, or List.
6+
7+
```TS
8+
import { spfi } from "@pnp/sp";
9+
import "@pnp/sp/webs";
10+
import { createChangeToken } from "@pnp/sp";
11+
import { dateAdd } from "@pnp/core";
12+
13+
const sp = spfi(...);
14+
15+
const w = await sp.web.select("Id")();
16+
17+
const token = createChangeToken("web", w.Id, dateAdd(new Date(), "day", -29));
18+
19+
const c = await sp.web.getChanges({
20+
ChangeTokenStart: token,
21+
});
22+
```

mkdocs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ nav:
8484
- teams: 'graph/teams.md'
8585
- to-do: 'graph/to-do.md'
8686
- users: 'graph/users.md'
87+
- workbooks: 'graph/workbooks.md'
8788
- logging:
8889
- logging: 'logging/index.md'
8990
- msaljsclient:
@@ -137,6 +138,7 @@ nav:
137138
- Subscriptions: 'sp/subscriptions.md'
138139
- 'Tenant Properties': 'sp/tenant-properties.md'
139140
- 'User custom actions': 'sp/user-custom-actions.md'
141+
- 'Utils': 'sp/utils.md'
140142
- Views: 'sp/views.md'
141143
- Webs: 'sp/webs.md'
142144
- sp-admin:
@@ -181,6 +183,8 @@ markdown_extensions:
181183
- admonition
182184
- toc:
183185
permalink: true
186+
- pymdownx.highlight
187+
- pymdownx.superfences
184188

185189
plugins:
186190
- search

0 commit comments

Comments
 (0)