@components-kit/open-workbook 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/assets/backend/dist/addin-rpc-client.d.ts +17 -0
- package/assets/backend/dist/addin-rpc-client.d.ts.map +1 -0
- package/assets/backend/dist/addin-rpc-client.js +66 -0
- package/assets/backend/dist/addin-rpc-client.js.map +1 -0
- package/assets/backend/dist/addin-websocket-server.d.ts +13 -0
- package/assets/backend/dist/addin-websocket-server.d.ts.map +1 -0
- package/assets/backend/dist/addin-websocket-server.js +199 -0
- package/assets/backend/dist/addin-websocket-server.js.map +1 -0
- package/assets/backend/dist/file-bridge.d.ts +2 -0
- package/assets/backend/dist/file-bridge.d.ts.map +1 -0
- package/assets/backend/dist/file-bridge.js +15 -0
- package/assets/backend/dist/file-bridge.js.map +1 -0
- package/assets/backend/dist/index.d.ts +4 -0
- package/assets/backend/dist/index.d.ts.map +1 -0
- package/assets/backend/dist/index.js +17 -0
- package/assets/backend/dist/index.js.map +1 -0
- package/assets/backend/dist/native-file-bridge-server.d.ts +28 -0
- package/assets/backend/dist/native-file-bridge-server.d.ts.map +1 -0
- package/assets/backend/dist/native-file-bridge-server.js +496 -0
- package/assets/backend/dist/native-file-bridge-server.js.map +1 -0
- package/assets/backend/dist/native-file-bridge.d.ts +20 -0
- package/assets/backend/dist/native-file-bridge.d.ts.map +1 -0
- package/assets/backend/dist/native-file-bridge.js +140 -0
- package/assets/backend/dist/native-file-bridge.js.map +1 -0
- package/assets/backend/dist/runtime-service.d.ts +3167 -0
- package/assets/backend/dist/runtime-service.d.ts.map +1 -0
- package/assets/backend/dist/runtime-service.js +6003 -0
- package/assets/backend/dist/runtime-service.js.map +1 -0
- package/assets/backend/dist/session-registry.d.ts +20 -0
- package/assets/backend/dist/session-registry.d.ts.map +1 -0
- package/assets/backend/dist/session-registry.js +53 -0
- package/assets/backend/dist/session-registry.js.map +1 -0
- package/assets/backend/dist/state-store.d.ts +26 -0
- package/assets/backend/dist/state-store.d.ts.map +1 -0
- package/assets/backend/dist/state-store.js +44 -0
- package/assets/backend/dist/state-store.js.map +1 -0
- package/assets/excel-addin/dist/connection.d.ts +26 -0
- package/assets/excel-addin/dist/connection.d.ts.map +1 -0
- package/assets/excel-addin/dist/connection.js +320 -0
- package/assets/excel-addin/dist/connection.js.map +1 -0
- package/assets/excel-addin/dist/excel-executor.d.ts +225 -0
- package/assets/excel-addin/dist/excel-executor.d.ts.map +1 -0
- package/assets/excel-addin/dist/excel-executor.js +2487 -0
- package/assets/excel-addin/dist/excel-executor.js.map +1 -0
- package/assets/excel-addin/dist/taskpane.d.ts +2 -0
- package/assets/excel-addin/dist/taskpane.d.ts.map +1 -0
- package/assets/excel-addin/dist/taskpane.js +28 -0
- package/assets/excel-addin/dist/taskpane.js.map +1 -0
- package/assets/excel-addin/manifest.xml +93 -0
- package/assets/excel-addin/public/taskpane.css +47 -0
- package/assets/excel-addin/public/taskpane.html +35 -0
- package/assets/excel-addin/scripts/dev-server.mjs +128 -0
- package/assets/instructions/open-workbook-excel/SKILL.md +52 -0
- package/assets/instructions/open-workbook-excel/references/multi-agent.md +43 -0
- package/assets/instructions/open-workbook-excel/references/performance.md +41 -0
- package/assets/instructions/open-workbook-excel/references/reliability.md +76 -0
- package/assets/instructions/open-workbook-excel/references/tool-selection.md +82 -0
- package/assets/instructions/open-workbook-excel/references/workflows.md +93 -0
- package/assets/mcp-server/dist/catalog.d.ts +5 -0
- package/assets/mcp-server/dist/catalog.d.ts.map +1 -0
- package/assets/mcp-server/dist/catalog.js +8 -0
- package/assets/mcp-server/dist/catalog.js.map +1 -0
- package/assets/mcp-server/dist/index.d.ts +3 -0
- package/assets/mcp-server/dist/index.d.ts.map +1 -0
- package/assets/mcp-server/dist/index.js +3779 -0
- package/assets/mcp-server/dist/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +721 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,2487 @@
|
|
|
1
|
+
import { chunkMatrixRows, createRangeFingerprint, createWorkbookFingerprint, formatA1Cell, hashStable, parseA1Address } from "@components-kit/open-workbook-excel-core";
|
|
2
|
+
import { runtimeError } from "@components-kit/open-workbook-protocol";
|
|
3
|
+
const ENGINE_NAME = "office-js-addin";
|
|
4
|
+
const ENGINE_VERSION = "0.1.0";
|
|
5
|
+
const CHUNK_CELL_LIMIT = 50_000;
|
|
6
|
+
const OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE = "https://open-workbook.dev/schema/local-config/1";
|
|
7
|
+
const EXCEL_API_VERSIONS = ["1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "1.10", "1.11", "1.12", "1.13", "1.14", "1.15", "1.16", "1.17"];
|
|
8
|
+
export function getRuntimeCapabilities() {
|
|
9
|
+
const apiSets = EXCEL_API_VERSIONS.map((version) => ({
|
|
10
|
+
set: "ExcelApi",
|
|
11
|
+
version,
|
|
12
|
+
supported: isOfficeApiSetSupported("ExcelApi", version)
|
|
13
|
+
}));
|
|
14
|
+
const supports = (version) => apiSets.some((apiSet) => apiSet.version === version && apiSet.supported);
|
|
15
|
+
const officeVersion = typeof Office.context.diagnostics?.version === "string" ? Office.context.diagnostics.version : undefined;
|
|
16
|
+
const platform = detectPlatform();
|
|
17
|
+
const supportsCompressedFileExport = typeof Office.context.document?.getFileAsync === "function" && platform !== "web";
|
|
18
|
+
return {
|
|
19
|
+
engine: {
|
|
20
|
+
name: ENGINE_NAME,
|
|
21
|
+
version: ENGINE_VERSION,
|
|
22
|
+
platform,
|
|
23
|
+
host: String(Office.context.host ?? "Excel"),
|
|
24
|
+
...(officeVersion !== undefined ? { officeVersion } : {})
|
|
25
|
+
},
|
|
26
|
+
apiSets,
|
|
27
|
+
capabilities: [
|
|
28
|
+
{
|
|
29
|
+
name: "workbook.context",
|
|
30
|
+
supported: supports("1.1"),
|
|
31
|
+
platforms: ["mac", "windows", "web"],
|
|
32
|
+
requires: [{ set: "ExcelApi", version: "1.1" }]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "range.batch.read_write",
|
|
36
|
+
supported: supports("1.9"),
|
|
37
|
+
platforms: ["mac", "windows", "web"],
|
|
38
|
+
requires: [{ set: "ExcelApi", version: "1.9" }]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "table.native",
|
|
42
|
+
supported: supports("1.9"),
|
|
43
|
+
platforms: ["mac", "windows", "web"],
|
|
44
|
+
requires: [{ set: "ExcelApi", version: "1.9" }]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "pivot.native",
|
|
48
|
+
supported: supports("1.8"),
|
|
49
|
+
platforms: ["mac", "windows", "web"],
|
|
50
|
+
requires: [{ set: "ExcelApi", version: "1.8" }]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "chart.native",
|
|
54
|
+
supported: supports("1.9"),
|
|
55
|
+
platforms: ["mac", "windows", "web"],
|
|
56
|
+
requires: [{ set: "ExcelApi", version: "1.9" }]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "range.metadata.advanced",
|
|
60
|
+
supported: supports("1.9"),
|
|
61
|
+
platforms: ["mac", "windows", "web"],
|
|
62
|
+
requires: [{ set: "ExcelApi", version: "1.9" }]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
hostCapabilities: [
|
|
66
|
+
hostCapability("range-values-formulas-styles", supports("1.9"), "ExcelApi", "1.9"),
|
|
67
|
+
hostCapability("tables-filters-sorts", supports("1.9"), "ExcelApi", "1.9"),
|
|
68
|
+
hostCapability("pivots", supports("1.8"), "ExcelApi", "1.8"),
|
|
69
|
+
hostCapability("charts", supports("1.9"), "ExcelApi", "1.9"),
|
|
70
|
+
{
|
|
71
|
+
name: "workbook-compressed-file-export",
|
|
72
|
+
supported: supportsCompressedFileExport,
|
|
73
|
+
status: supportsCompressedFileExport ? "supported" : "unsupported",
|
|
74
|
+
reason: supportsCompressedFileExport
|
|
75
|
+
? "Office Document.getFileAsync supports compressed workbook slices on this host."
|
|
76
|
+
: "Compressed workbook file export is supported by Excel desktop hosts, not Excel on the web."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "workbook-save-as-local-path",
|
|
80
|
+
supported: false,
|
|
81
|
+
status: "unsupported",
|
|
82
|
+
reason: "Office.js does not expose a deterministic local Save As path API."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "theme-freeze-print-layout-replay",
|
|
86
|
+
supported: false,
|
|
87
|
+
status: "limited",
|
|
88
|
+
reason: "Current implementation reports layout capability status instead of replaying these dimensions."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "comments-notes-address-mapping",
|
|
92
|
+
supported: false,
|
|
93
|
+
status: "limited",
|
|
94
|
+
reason: "Office.js comment and legacy-note collections need deterministic address mapping before agent writes are enabled."
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function getActiveWorkbookContext() {
|
|
100
|
+
return Excel.run(async (context) => {
|
|
101
|
+
const workbook = context.workbook;
|
|
102
|
+
const activeWorksheet = workbook.worksheets.getActiveWorksheet();
|
|
103
|
+
workbook.load("name");
|
|
104
|
+
activeWorksheet.load("name");
|
|
105
|
+
await context.sync();
|
|
106
|
+
return {
|
|
107
|
+
workbookId: workbook.name,
|
|
108
|
+
name: workbook.name,
|
|
109
|
+
platform: detectPlatform()
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export async function getSelection() {
|
|
114
|
+
return Excel.run(async (context) => {
|
|
115
|
+
const workbook = context.workbook;
|
|
116
|
+
const selectedRange = workbook.getSelectedRange();
|
|
117
|
+
selectedRange.load("address,rowIndex,columnIndex,rowCount,columnCount");
|
|
118
|
+
selectedRange.worksheet.load("name");
|
|
119
|
+
workbook.load("name");
|
|
120
|
+
await context.sync();
|
|
121
|
+
const workbookRef = {
|
|
122
|
+
workbookId: workbook.name,
|
|
123
|
+
name: workbook.name,
|
|
124
|
+
platform: detectPlatform()
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
workbook: workbookRef,
|
|
128
|
+
selection: {
|
|
129
|
+
workbookId: workbookRef.workbookId,
|
|
130
|
+
sheetName: selectedRange.worksheet.name,
|
|
131
|
+
address: stripSheetName(selectedRange.address),
|
|
132
|
+
startCell: cellPositionFromZeroBased(workbookRef.workbookId, selectedRange.worksheet.name, selectedRange.rowIndex, selectedRange.columnIndex),
|
|
133
|
+
endCell: cellPositionFromZeroBased(workbookRef.workbookId, selectedRange.worksheet.name, selectedRange.rowIndex + selectedRange.rowCount - 1, selectedRange.columnIndex + selectedRange.columnCount - 1),
|
|
134
|
+
rowCount: selectedRange.rowCount,
|
|
135
|
+
columnCount: selectedRange.columnCount,
|
|
136
|
+
cellCount: selectedRange.rowCount * selectedRange.columnCount,
|
|
137
|
+
isSingleCell: selectedRange.rowCount === 1 && selectedRange.columnCount === 1
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export async function setActiveSheet(sheetName) {
|
|
143
|
+
return Excel.run(async (context) => {
|
|
144
|
+
const worksheet = context.workbook.worksheets.getItem(sheetName);
|
|
145
|
+
worksheet.activate();
|
|
146
|
+
await context.sync();
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
activeSheet: sheetName
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function getWorkbookInfo() {
|
|
154
|
+
return Excel.run(async (context) => {
|
|
155
|
+
const workbook = context.workbook;
|
|
156
|
+
const activeWorksheet = workbook.worksheets.getActiveWorksheet();
|
|
157
|
+
workbook.load("name");
|
|
158
|
+
activeWorksheet.load("name");
|
|
159
|
+
workbook.worksheets.load("items/name");
|
|
160
|
+
await context.sync();
|
|
161
|
+
return {
|
|
162
|
+
workbook: {
|
|
163
|
+
workbookId: workbook.name,
|
|
164
|
+
name: workbook.name,
|
|
165
|
+
platform: detectPlatform()
|
|
166
|
+
},
|
|
167
|
+
activeSheet: activeWorksheet.name,
|
|
168
|
+
worksheetCount: workbook.worksheets.items.length
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export async function getWorkbookMap() {
|
|
173
|
+
return Excel.run(async (context) => {
|
|
174
|
+
const workbook = context.workbook;
|
|
175
|
+
workbook.load("name");
|
|
176
|
+
workbook.worksheets.load("items/name,items/position,items/visibility");
|
|
177
|
+
await context.sync();
|
|
178
|
+
const loaded = workbook.worksheets.items.map((worksheet) => {
|
|
179
|
+
const usedRange = worksheet.getUsedRangeOrNullObject();
|
|
180
|
+
const tables = worksheet.tables;
|
|
181
|
+
usedRange.load("address,rowCount,columnCount");
|
|
182
|
+
tables.load("items/name");
|
|
183
|
+
return { worksheet, usedRange, tables };
|
|
184
|
+
});
|
|
185
|
+
await context.sync();
|
|
186
|
+
return {
|
|
187
|
+
workbook: {
|
|
188
|
+
workbookId: workbook.name,
|
|
189
|
+
name: workbook.name,
|
|
190
|
+
platform: detectPlatform()
|
|
191
|
+
},
|
|
192
|
+
sheets: loaded.map(({ worksheet, usedRange, tables }) => {
|
|
193
|
+
const sheet = {
|
|
194
|
+
name: worksheet.name,
|
|
195
|
+
position: worksheet.position,
|
|
196
|
+
visibility: String(worksheet.visibility),
|
|
197
|
+
tables: tables.items.map((table) => ({ name: table.name }))
|
|
198
|
+
};
|
|
199
|
+
if (!usedRange.isNullObject) {
|
|
200
|
+
sheet.usedRange = {
|
|
201
|
+
address: usedRange.address,
|
|
202
|
+
rowCount: usedRange.rowCount,
|
|
203
|
+
columnCount: usedRange.columnCount
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return sheet;
|
|
207
|
+
})
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export async function calculateWorkbook(calculationType = "full") {
|
|
212
|
+
return Excel.run(async (context) => {
|
|
213
|
+
context.workbook.application.calculate(calculationType === "recalculate" ? Excel.CalculationType.recalculate : Excel.CalculationType.full);
|
|
214
|
+
await context.sync();
|
|
215
|
+
return { ok: true, calculationType };
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
export async function saveWorkbook() {
|
|
219
|
+
return Excel.run(async (context) => {
|
|
220
|
+
context.workbook.save(Excel.SaveBehavior.save);
|
|
221
|
+
await context.sync();
|
|
222
|
+
return { ok: true };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export async function exportWorkbookFile(workbookId, sliceSize = 4 * 1024 * 1024) {
|
|
226
|
+
const document = Office.context.document;
|
|
227
|
+
if (!document || typeof document.getFileAsync !== "function") {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
error: runtimeError("CAPABILITY_UNAVAILABLE", "This Excel host does not expose Office Document.getFileAsync.", { retryable: false })
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (detectPlatform() === "web") {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: runtimeError("CAPABILITY_UNAVAILABLE", "Excel on the web does not expose compressed workbook export through Office.js.", { retryable: false })
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const file = await getDocumentFile(sliceSize);
|
|
240
|
+
try {
|
|
241
|
+
const chunks = [];
|
|
242
|
+
for (let index = 0; index < file.sliceCount; index += 1) {
|
|
243
|
+
const slice = await getDocumentFileSlice(file, index);
|
|
244
|
+
chunks.push(sliceDataToBase64(slice.data));
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
workbookId: workbookId,
|
|
249
|
+
fileType: "compressed",
|
|
250
|
+
size: file.size,
|
|
251
|
+
sliceCount: file.sliceCount,
|
|
252
|
+
base64: chunks.join(""),
|
|
253
|
+
capturedAt: new Date().toISOString()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
await closeDocumentFile(file);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export async function closeWorkbook(closeBehavior = "Save") {
|
|
261
|
+
return Excel.run(async (context) => {
|
|
262
|
+
context.workbook.close(closeBehavior);
|
|
263
|
+
await context.sync();
|
|
264
|
+
return { ok: true, closeBehavior };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
export async function listNames(workbookId) {
|
|
268
|
+
return Excel.run(async (context) => {
|
|
269
|
+
const workbookNames = context.workbook.names;
|
|
270
|
+
workbookNames.load("items/name,items/scope,items/type,items/value,items/formula,items/comment,items/visible");
|
|
271
|
+
context.workbook.worksheets.load("items/name");
|
|
272
|
+
await context.sync();
|
|
273
|
+
const worksheetCollections = context.workbook.worksheets.items.map((worksheet) => {
|
|
274
|
+
worksheet.names.load("items/name,items/scope,items/type,items/value,items/formula,items/comment,items/visible");
|
|
275
|
+
return { worksheet, names: worksheet.names };
|
|
276
|
+
});
|
|
277
|
+
await context.sync();
|
|
278
|
+
const names = [
|
|
279
|
+
...workbookNames.items.map((item) => materializeNameInfo(workbookId, item)),
|
|
280
|
+
...worksheetCollections.flatMap(({ worksheet, names }) => names.items.map((item) => materializeNameInfo(workbookId, item, worksheet.name)))
|
|
281
|
+
];
|
|
282
|
+
return { ok: true, names };
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
export async function getName(request) {
|
|
286
|
+
return Excel.run(async (context) => {
|
|
287
|
+
const item = getNamedItem(context, request);
|
|
288
|
+
const range = loadNameWithRange(item);
|
|
289
|
+
await context.sync();
|
|
290
|
+
if (item.isNullObject) {
|
|
291
|
+
return { ok: false };
|
|
292
|
+
}
|
|
293
|
+
return { ok: true, name: materializeNameInfo(request.workbookId, item, request.sheetName, range) };
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
export async function createName(request) {
|
|
297
|
+
return Excel.run(async (context) => {
|
|
298
|
+
const collection = request.sheetName ? context.workbook.worksheets.getItem(request.sheetName).names : context.workbook.names;
|
|
299
|
+
const item = collection.add(request.name, nameReference(context, request), request.comment);
|
|
300
|
+
if (request.visible !== undefined) {
|
|
301
|
+
item.visible = request.visible;
|
|
302
|
+
}
|
|
303
|
+
const range = loadNameWithRange(item);
|
|
304
|
+
await context.sync();
|
|
305
|
+
return { ok: true, name: materializeNameInfo(request.workbookId, item, request.sheetName, range) };
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
export async function updateName(request) {
|
|
309
|
+
return Excel.run(async (context) => {
|
|
310
|
+
const item = getNamedItem(context, request);
|
|
311
|
+
item.load("name");
|
|
312
|
+
await context.sync();
|
|
313
|
+
if (item.isNullObject) {
|
|
314
|
+
return { ok: false };
|
|
315
|
+
}
|
|
316
|
+
if (request.reference !== undefined || request.formula !== undefined) {
|
|
317
|
+
item.formula = nameFormula(request);
|
|
318
|
+
}
|
|
319
|
+
if (request.comment !== undefined) {
|
|
320
|
+
item.comment = request.comment;
|
|
321
|
+
}
|
|
322
|
+
if (request.visible !== undefined) {
|
|
323
|
+
item.visible = request.visible;
|
|
324
|
+
}
|
|
325
|
+
const range = loadNameWithRange(item);
|
|
326
|
+
await context.sync();
|
|
327
|
+
return { ok: true, name: materializeNameInfo(request.workbookId, item, request.sheetName, range) };
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
export async function deleteName(request) {
|
|
331
|
+
return Excel.run(async (context) => {
|
|
332
|
+
const item = getNamedItem(context, request);
|
|
333
|
+
item.load("name");
|
|
334
|
+
await context.sync();
|
|
335
|
+
if (item.isNullObject) {
|
|
336
|
+
return { ok: false, deleted: false };
|
|
337
|
+
}
|
|
338
|
+
item.delete();
|
|
339
|
+
await context.sync();
|
|
340
|
+
return { ok: true, deleted: true };
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
export async function embedWorkbookLocalConfig(request) {
|
|
344
|
+
return Excel.run(async (context) => {
|
|
345
|
+
const customXmlParts = getCustomXmlParts(context);
|
|
346
|
+
if (!customXmlParts) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
embedded: false,
|
|
350
|
+
partCount: 0,
|
|
351
|
+
namespaceUri: OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE,
|
|
352
|
+
error: runtimeError("CAPABILITY_UNAVAILABLE", "This Excel host does not expose workbook custom XML parts to Office.js.", { retryable: false })
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const existing = customXmlParts.getByNamespace(OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE);
|
|
356
|
+
existing.load("items/id");
|
|
357
|
+
await context.sync();
|
|
358
|
+
for (const part of existing.items ?? []) {
|
|
359
|
+
part.delete();
|
|
360
|
+
}
|
|
361
|
+
await context.sync();
|
|
362
|
+
customXmlParts.add(workbookLocalConfigXml(request.config));
|
|
363
|
+
await context.sync();
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
embedded: true,
|
|
367
|
+
partCount: 1,
|
|
368
|
+
namespaceUri: OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
export async function readWorkbookEmbeddedLocalConfig(workbookId) {
|
|
373
|
+
return Excel.run(async (context) => {
|
|
374
|
+
const customXmlParts = getCustomXmlParts(context);
|
|
375
|
+
if (!customXmlParts) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
workbookId,
|
|
379
|
+
embedded: false,
|
|
380
|
+
partCount: 0,
|
|
381
|
+
namespaceUri: OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE,
|
|
382
|
+
error: runtimeError("CAPABILITY_UNAVAILABLE", "This Excel host does not expose workbook custom XML parts to Office.js.", { retryable: false })
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const parts = customXmlParts.getByNamespace(OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE);
|
|
386
|
+
parts.load("items/id");
|
|
387
|
+
await context.sync();
|
|
388
|
+
if (!parts.items || parts.items.length === 0) {
|
|
389
|
+
return {
|
|
390
|
+
ok: true,
|
|
391
|
+
workbookId,
|
|
392
|
+
embedded: false,
|
|
393
|
+
partCount: 0,
|
|
394
|
+
namespaceUri: OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const xmlResult = parts.items[0].getXml();
|
|
398
|
+
await context.sync();
|
|
399
|
+
const config = parseWorkbookLocalConfigXml(xmlResult.value);
|
|
400
|
+
return {
|
|
401
|
+
ok: true,
|
|
402
|
+
workbookId,
|
|
403
|
+
embedded: true,
|
|
404
|
+
partCount: parts.items.length,
|
|
405
|
+
config,
|
|
406
|
+
namespaceUri: OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
export async function listPivotTables(workbookId) {
|
|
411
|
+
return Excel.run(async (context) => {
|
|
412
|
+
const pivots = context.workbook.pivotTables;
|
|
413
|
+
pivots.load("items/name");
|
|
414
|
+
await context.sync();
|
|
415
|
+
const pivotTables = [];
|
|
416
|
+
for (const pivot of pivots.items) {
|
|
417
|
+
pivotTables.push(await readPivotTableInfo(context, workbookId, pivot));
|
|
418
|
+
}
|
|
419
|
+
return { ok: true, pivotTables };
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
export async function getPivotTableInfo(request) {
|
|
423
|
+
return Excel.run(async (context) => {
|
|
424
|
+
const pivot = context.workbook.pivotTables.getItemOrNullObject(request.pivotTableName);
|
|
425
|
+
pivot.load("isNullObject");
|
|
426
|
+
await context.sync();
|
|
427
|
+
if (pivot.isNullObject) {
|
|
428
|
+
return { ok: false };
|
|
429
|
+
}
|
|
430
|
+
return { ok: true, info: await readPivotTableInfo(context, request.workbookId, pivot) };
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
export async function createPivotTable(request) {
|
|
434
|
+
return Excel.run(async (context) => {
|
|
435
|
+
const source = request.sourceTableName
|
|
436
|
+
? context.workbook.tables.getItem(request.sourceTableName)
|
|
437
|
+
: context.workbook.worksheets.getItem(request.sourceSheetName ?? request.destinationSheetName).getRange(stripSheetName(request.sourceAddress ?? ""));
|
|
438
|
+
const destination = context.workbook.worksheets.getItem(request.destinationSheetName).getRange(stripSheetName(request.destinationAddress));
|
|
439
|
+
const pivot = context.workbook.pivotTables.add(request.pivotTableName, source, destination);
|
|
440
|
+
await context.sync();
|
|
441
|
+
if (hasPivotCreateLayout(request)) {
|
|
442
|
+
loadPivotTemplateReplayObjects(pivot);
|
|
443
|
+
await context.sync();
|
|
444
|
+
applyPivotCreateLayout(request, pivot);
|
|
445
|
+
await context.sync();
|
|
446
|
+
if (request.refresh !== false) {
|
|
447
|
+
pivot.refresh();
|
|
448
|
+
await context.sync();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { ok: true, info: await readPivotTableInfo(context, request.workbookId, pivot) };
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
export async function refreshPivotTable(request) {
|
|
455
|
+
return Excel.run(async (context) => {
|
|
456
|
+
const pivot = context.workbook.pivotTables.getItemOrNullObject(request.pivotTableName);
|
|
457
|
+
pivot.load("isNullObject");
|
|
458
|
+
await context.sync();
|
|
459
|
+
if (pivot.isNullObject) {
|
|
460
|
+
return { ok: false };
|
|
461
|
+
}
|
|
462
|
+
pivot.refresh();
|
|
463
|
+
await context.sync();
|
|
464
|
+
return { ok: true, info: await readPivotTableInfo(context, request.workbookId, pivot) };
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
export async function refreshAllPivotTables(workbookId) {
|
|
468
|
+
return Excel.run(async (context) => {
|
|
469
|
+
context.workbook.pivotTables.refreshAll();
|
|
470
|
+
await context.sync();
|
|
471
|
+
return { ok: true };
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
export async function copyPivotTableFromTemplate(request) {
|
|
475
|
+
return Excel.run(async (context) => {
|
|
476
|
+
const sourcePivot = context.workbook.pivotTables.getItemOrNullObject(request.templatePivotTableName);
|
|
477
|
+
const targetPivot = context.workbook.pivotTables.getItemOrNullObject(request.pivotTableName);
|
|
478
|
+
sourcePivot.load("isNullObject");
|
|
479
|
+
targetPivot.load("isNullObject");
|
|
480
|
+
await context.sync();
|
|
481
|
+
if (sourcePivot.isNullObject || targetPivot.isNullObject) {
|
|
482
|
+
return { ok: false, copied: [] };
|
|
483
|
+
}
|
|
484
|
+
const source = await readPivotTableInfo(context, request.workbookId, sourcePivot);
|
|
485
|
+
loadPivotTemplateReplayObjects(targetPivot);
|
|
486
|
+
await context.sync();
|
|
487
|
+
const copied = applyPivotTemplateMetadata(source, targetPivot, request.dimensions);
|
|
488
|
+
await context.sync();
|
|
489
|
+
if (!request.dimensions || request.dimensions.includes("refresh")) {
|
|
490
|
+
targetPivot.refresh();
|
|
491
|
+
await context.sync();
|
|
492
|
+
}
|
|
493
|
+
const target = await readPivotTableInfo(context, request.workbookId, targetPivot);
|
|
494
|
+
return {
|
|
495
|
+
ok: true,
|
|
496
|
+
copied,
|
|
497
|
+
source,
|
|
498
|
+
target
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
export async function deletePivotTable(request) {
|
|
503
|
+
return Excel.run(async (context) => {
|
|
504
|
+
const pivot = context.workbook.pivotTables.getItemOrNullObject(request.pivotTableName);
|
|
505
|
+
pivot.load("name,isNullObject");
|
|
506
|
+
await context.sync();
|
|
507
|
+
if (pivot.isNullObject) {
|
|
508
|
+
return { ok: false, deleted: false };
|
|
509
|
+
}
|
|
510
|
+
pivot.delete();
|
|
511
|
+
await context.sync();
|
|
512
|
+
return { ok: true, deleted: true };
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
export async function listCharts(workbookId) {
|
|
516
|
+
return Excel.run(async (context) => {
|
|
517
|
+
context.workbook.worksheets.load("items/name");
|
|
518
|
+
await context.sync();
|
|
519
|
+
for (const worksheet of context.workbook.worksheets.items) {
|
|
520
|
+
worksheet.charts.load("items/name,items/id,items/chartType,items/top,items/left,items/width,items/height,items/style,items/plotBy");
|
|
521
|
+
}
|
|
522
|
+
await context.sync();
|
|
523
|
+
const loaded = context.workbook.worksheets.items.flatMap((worksheet) => worksheet.charts.items.map((chart) => loadChartInfoObjects(workbookId, worksheet.name, chart)));
|
|
524
|
+
return { ok: true, charts: loaded.map(materializeChartInfo) };
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
export async function getChartInfo(request) {
|
|
528
|
+
return Excel.run(async (context) => {
|
|
529
|
+
const chart = context.workbook.worksheets.getItem(request.sheetName).charts.getItemOrNullObject(request.chartName);
|
|
530
|
+
chart.load("name,id,chartType,top,left,width,height,style,plotBy,isNullObject");
|
|
531
|
+
const loaded = loadChartInfoObjects(request.workbookId, request.sheetName, chart);
|
|
532
|
+
await context.sync();
|
|
533
|
+
if (chart.isNullObject) {
|
|
534
|
+
return { ok: false };
|
|
535
|
+
}
|
|
536
|
+
return { ok: true, info: materializeChartInfo(loaded) };
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
export async function createChart(request) {
|
|
540
|
+
return Excel.run(async (context) => {
|
|
541
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
542
|
+
const source = worksheet.getRange(stripSheetName(request.sourceAddress));
|
|
543
|
+
const chart = worksheet.charts.add(request.chartType, source, request.seriesBy);
|
|
544
|
+
if (request.chartName !== undefined) {
|
|
545
|
+
chart.name = request.chartName;
|
|
546
|
+
}
|
|
547
|
+
if (request.title !== undefined) {
|
|
548
|
+
chart.title.text = request.title;
|
|
549
|
+
}
|
|
550
|
+
if (request.style !== undefined) {
|
|
551
|
+
chart.style = request.style;
|
|
552
|
+
}
|
|
553
|
+
if (request.position !== undefined) {
|
|
554
|
+
chart.setPosition(request.position.startCell, request.position.endCell);
|
|
555
|
+
}
|
|
556
|
+
chart.load("name,id,chartType,top,left,width,height,style,plotBy");
|
|
557
|
+
const loaded = loadChartInfoObjects(request.workbookId, request.sheetName, chart);
|
|
558
|
+
await context.sync();
|
|
559
|
+
return { ok: true, info: materializeChartInfo(loaded) };
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
export async function updateChartDataSource(request) {
|
|
563
|
+
return Excel.run(async (context) => {
|
|
564
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
565
|
+
const chart = worksheet.charts.getItemOrNullObject(request.chartName);
|
|
566
|
+
chart.load("name,id,chartType,top,left,width,height,style,plotBy,isNullObject");
|
|
567
|
+
const loaded = loadChartInfoObjects(request.workbookId, request.sheetName, chart);
|
|
568
|
+
await context.sync();
|
|
569
|
+
if (chart.isNullObject) {
|
|
570
|
+
return { ok: false };
|
|
571
|
+
}
|
|
572
|
+
chart.setData(worksheet.getRange(stripSheetName(request.sourceAddress)), request.seriesBy);
|
|
573
|
+
await context.sync();
|
|
574
|
+
return { ok: true, info: materializeChartInfo(loaded) };
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
export async function copyChartFromTemplate(request) {
|
|
578
|
+
return Excel.run(async (context) => {
|
|
579
|
+
const sourceChart = context.workbook.worksheets.getItem(request.templateSheetName).charts.getItemOrNullObject(request.templateChartName);
|
|
580
|
+
const targetChart = context.workbook.worksheets.getItem(request.sheetName).charts.getItemOrNullObject(request.chartName);
|
|
581
|
+
sourceChart.load("name,id,chartType,top,left,width,height,style,plotBy,isNullObject");
|
|
582
|
+
sourceChart.title.load("text");
|
|
583
|
+
targetChart.load("name,id,chartType,top,left,width,height,style,plotBy,isNullObject");
|
|
584
|
+
targetChart.title.load("text");
|
|
585
|
+
await context.sync();
|
|
586
|
+
if (sourceChart.isNullObject || targetChart.isNullObject) {
|
|
587
|
+
return { ok: false, copied: [] };
|
|
588
|
+
}
|
|
589
|
+
const copied = [];
|
|
590
|
+
targetChart.chartType = sourceChart.chartType;
|
|
591
|
+
copied.push("chartType");
|
|
592
|
+
if (sourceChart.style !== undefined) {
|
|
593
|
+
targetChart.style = sourceChart.style;
|
|
594
|
+
copied.push("style");
|
|
595
|
+
}
|
|
596
|
+
if (sourceChart.title.text !== undefined) {
|
|
597
|
+
targetChart.title.text = sourceChart.title.text;
|
|
598
|
+
copied.push("title");
|
|
599
|
+
}
|
|
600
|
+
targetChart.top = sourceChart.top;
|
|
601
|
+
targetChart.left = sourceChart.left;
|
|
602
|
+
targetChart.width = sourceChart.width;
|
|
603
|
+
targetChart.height = sourceChart.height;
|
|
604
|
+
copied.push("position");
|
|
605
|
+
targetChart.load("name,id,chartType,top,left,width,height,style,plotBy");
|
|
606
|
+
targetChart.title.load("text");
|
|
607
|
+
await context.sync();
|
|
608
|
+
return {
|
|
609
|
+
ok: true,
|
|
610
|
+
copied,
|
|
611
|
+
source: materializeChartInfo({ workbookId: request.workbookId, sheetName: request.templateSheetName, chart: sourceChart }),
|
|
612
|
+
target: materializeChartInfo({ workbookId: request.workbookId, sheetName: request.sheetName, chart: targetChart })
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
export async function refreshChart(request) {
|
|
617
|
+
return getChartInfo(request);
|
|
618
|
+
}
|
|
619
|
+
export async function deleteChart(request) {
|
|
620
|
+
return Excel.run(async (context) => {
|
|
621
|
+
const chart = context.workbook.worksheets.getItem(request.sheetName).charts.getItemOrNullObject(request.chartName);
|
|
622
|
+
chart.load("name,isNullObject");
|
|
623
|
+
await context.sync();
|
|
624
|
+
if (chart.isNullObject) {
|
|
625
|
+
return { ok: false, deleted: false };
|
|
626
|
+
}
|
|
627
|
+
chart.delete();
|
|
628
|
+
await context.sync();
|
|
629
|
+
return { ok: true, deleted: true };
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
export async function listTables(workbookId) {
|
|
633
|
+
return Excel.run(async (context) => {
|
|
634
|
+
const tables = context.workbook.tables;
|
|
635
|
+
tables.load("items/name,items/id,items/style,items/showHeaders,items/showTotals,items/showFilterButton,items/showBandedRows,items/showBandedColumns");
|
|
636
|
+
await context.sync();
|
|
637
|
+
const loaded = tables.items.map((table) => loadTableInfoObjects(table));
|
|
638
|
+
await context.sync();
|
|
639
|
+
return {
|
|
640
|
+
ok: true,
|
|
641
|
+
tables: loaded.map((loadedTable) => materializeTableInfo(workbookId, loadedTable))
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
export async function getTableInfo(request) {
|
|
646
|
+
return Excel.run(async (context) => {
|
|
647
|
+
const table = context.workbook.tables.getItem(request.tableName);
|
|
648
|
+
const loaded = loadTableInfoObjects(table);
|
|
649
|
+
await context.sync();
|
|
650
|
+
return { ok: true, info: materializeTableInfo(request.workbookId, loaded) };
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
export async function readTable(request) {
|
|
654
|
+
return Excel.run(async (context) => {
|
|
655
|
+
const table = context.workbook.tables.getItem(request.tableName);
|
|
656
|
+
const loaded = loadTableInfoObjects(table);
|
|
657
|
+
await context.sync();
|
|
658
|
+
const headerRange = table.getHeaderRowRange();
|
|
659
|
+
headerRange.load("values");
|
|
660
|
+
const dataRange = table.rows.count > 0 ? table.getDataBodyRange() : undefined;
|
|
661
|
+
if (dataRange) {
|
|
662
|
+
dataRange.load("values,formulas,text,numberFormat");
|
|
663
|
+
}
|
|
664
|
+
await context.sync();
|
|
665
|
+
return {
|
|
666
|
+
ok: true,
|
|
667
|
+
table: {
|
|
668
|
+
info: materializeTableInfo(request.workbookId, loaded),
|
|
669
|
+
headers: headerRange.values,
|
|
670
|
+
values: (dataRange?.values ?? []),
|
|
671
|
+
formulas: (dataRange?.formulas ?? []),
|
|
672
|
+
text: (dataRange?.text ?? []),
|
|
673
|
+
numberFormat: (dataRange?.numberFormat ?? [])
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
export async function createTable(request) {
|
|
679
|
+
return Excel.run(async (context) => {
|
|
680
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
681
|
+
const range = worksheet.getRange(stripSheetName(request.address));
|
|
682
|
+
if (request.values) {
|
|
683
|
+
range.values = request.values;
|
|
684
|
+
}
|
|
685
|
+
const table = worksheet.tables.add(range, request.hasHeaders);
|
|
686
|
+
if (request.tableName) {
|
|
687
|
+
table.name = request.tableName;
|
|
688
|
+
}
|
|
689
|
+
if (request.style) {
|
|
690
|
+
table.style = request.style;
|
|
691
|
+
}
|
|
692
|
+
if (request.showTotals !== undefined) {
|
|
693
|
+
table.showTotals = request.showTotals;
|
|
694
|
+
}
|
|
695
|
+
const loaded = loadTableInfoObjects(table);
|
|
696
|
+
await context.sync();
|
|
697
|
+
return { ok: true, info: materializeTableInfo(request.workbookId, loaded) };
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
export async function resizeTable(request) {
|
|
701
|
+
return mutateTableAndReturnInfo(request, (table) => table.resize(request.address));
|
|
702
|
+
}
|
|
703
|
+
export async function appendTableRows(request) {
|
|
704
|
+
return mutateTableAndReturnInfo(request, (table) => {
|
|
705
|
+
table.rows.add(request.index ?? -1, request.values, request.alwaysInsert ?? true);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
export async function updateTableRows(request) {
|
|
709
|
+
return mutateTableAndReturnInfo(request, (table) => {
|
|
710
|
+
for (const row of request.rows) {
|
|
711
|
+
table.rows.getItemAt(row.index).values = [row.values];
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
export async function clearTableDataKeepFormulas(request) {
|
|
716
|
+
return Excel.run(async (context) => {
|
|
717
|
+
const table = context.workbook.tables.getItem(request.tableName);
|
|
718
|
+
table.rows.load("count");
|
|
719
|
+
await context.sync();
|
|
720
|
+
if (table.rows.count > 0) {
|
|
721
|
+
const range = table.getDataBodyRange();
|
|
722
|
+
range.load("formulas");
|
|
723
|
+
await context.sync();
|
|
724
|
+
range.formulas = range.formulas.map((row) => row.map((formula) => (typeof formula === "string" && formula.startsWith("=") ? formula : null)));
|
|
725
|
+
}
|
|
726
|
+
const loaded = loadTableInfoObjects(table);
|
|
727
|
+
await context.sync();
|
|
728
|
+
return { ok: true, info: materializeTableInfo(request.workbookId, loaded) };
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
export async function clearTableFilters(request) {
|
|
732
|
+
return mutateTableAndReturnInfo(request, (table) => table.clearFilters());
|
|
733
|
+
}
|
|
734
|
+
export async function applyTableFilters(request) {
|
|
735
|
+
return mutateTableAndReturnInfo(request, (table) => {
|
|
736
|
+
for (const filter of request.filters) {
|
|
737
|
+
table.columns.getItem(filter.column).filter.apply(filter.criteria);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
export async function sortTable(request) {
|
|
742
|
+
return mutateTableAndReturnInfo(request, (table) => {
|
|
743
|
+
table.sort.apply(request.fields, request.matchCase, request.method);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
export async function clearTableSort(request) {
|
|
747
|
+
return mutateTableAndReturnInfo(request, (table) => table.sort.clear());
|
|
748
|
+
}
|
|
749
|
+
export async function setTableTotalRow(request) {
|
|
750
|
+
return mutateTableAndReturnInfo(request, (table) => {
|
|
751
|
+
table.showTotals = request.showTotals;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
export async function setTableStyle(request) {
|
|
755
|
+
return mutateTableAndReturnInfo(request, (table) => {
|
|
756
|
+
table.style = request.style;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
export async function copyTableStructure(request) {
|
|
760
|
+
return Excel.run(async (context) => {
|
|
761
|
+
const sourceTable = context.workbook.tables.getItem(request.tableName);
|
|
762
|
+
const headerRange = sourceTable.getHeaderRowRange();
|
|
763
|
+
headerRange.load("values");
|
|
764
|
+
sourceTable.load("name,style,showTotals,showFilterButton,showBandedRows,showBandedColumns");
|
|
765
|
+
await context.sync();
|
|
766
|
+
const targetSheet = context.workbook.worksheets.getItem(request.targetSheetName);
|
|
767
|
+
const targetRange = targetSheet.getRange(stripSheetName(request.targetAddress));
|
|
768
|
+
targetRange.values = headerRange.values;
|
|
769
|
+
const table = targetSheet.tables.add(targetRange, true);
|
|
770
|
+
table.name = request.newTableName ?? `${sourceTable.name}_Copy`;
|
|
771
|
+
if (request.includeStyle ?? true) {
|
|
772
|
+
table.style = sourceTable.style;
|
|
773
|
+
table.showBandedRows = sourceTable.showBandedRows;
|
|
774
|
+
table.showBandedColumns = sourceTable.showBandedColumns;
|
|
775
|
+
table.showFilterButton = sourceTable.showFilterButton;
|
|
776
|
+
}
|
|
777
|
+
if (request.includeTotals ?? false) {
|
|
778
|
+
table.showTotals = sourceTable.showTotals;
|
|
779
|
+
}
|
|
780
|
+
const loaded = loadTableInfoObjects(table);
|
|
781
|
+
await context.sync();
|
|
782
|
+
return { ok: true, info: materializeTableInfo(request.workbookId, loaded) };
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
export async function snapshotRanges(workbookId, ranges) {
|
|
786
|
+
return Excel.run(async (context) => {
|
|
787
|
+
const loaded = [];
|
|
788
|
+
for (const target of ranges) {
|
|
789
|
+
const range = getRange(context, target);
|
|
790
|
+
loadSnapshotProperties(range);
|
|
791
|
+
loaded.push({ target, range });
|
|
792
|
+
}
|
|
793
|
+
const workbook = context.workbook;
|
|
794
|
+
workbook.load("name, worksheets/items/name");
|
|
795
|
+
await context.sync();
|
|
796
|
+
const rangeSnapshots = loaded.map(({ target, range }) => materializeSnapshot(target, range));
|
|
797
|
+
return {
|
|
798
|
+
workbookFingerprint: createWorkbookFingerprint(workbookId, {
|
|
799
|
+
workbookName: workbook.name,
|
|
800
|
+
ranges: rangeSnapshots.map((snapshot) => snapshot.fingerprint.hash)
|
|
801
|
+
}, {
|
|
802
|
+
sheets: workbook.worksheets.items.map((worksheet) => worksheet.name)
|
|
803
|
+
}),
|
|
804
|
+
rangeSnapshots
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
export async function readRangeHyperlinks(request) {
|
|
809
|
+
return readRangeMetadata(request, (range) => {
|
|
810
|
+
range.load("address,hyperlink");
|
|
811
|
+
}, (range) => range.hyperlink);
|
|
812
|
+
}
|
|
813
|
+
export async function readRangeDataValidation(request) {
|
|
814
|
+
return readRangeMetadata(request, (range) => {
|
|
815
|
+
range.load("address");
|
|
816
|
+
range.dataValidation.load("type,rule,prompt,errorAlert,ignoreBlanks,valid");
|
|
817
|
+
}, (range) => range.dataValidation.toJSON());
|
|
818
|
+
}
|
|
819
|
+
export async function readRangeConditionalFormatting(request) {
|
|
820
|
+
return readRangeMetadata(request, (range) => {
|
|
821
|
+
range.load("address");
|
|
822
|
+
range.conditionalFormats.load("items/id,items/type,items/priority,items/stopIfTrue");
|
|
823
|
+
}, (range) => range.conditionalFormats.toJSON());
|
|
824
|
+
}
|
|
825
|
+
export async function readRangeMergedCells(request) {
|
|
826
|
+
return Excel.run(async (context) => {
|
|
827
|
+
const target = targetFromMetadataRequest(request);
|
|
828
|
+
const range = getRange(context, target);
|
|
829
|
+
const merged = range.getMergedAreasOrNullObject();
|
|
830
|
+
merged.load("address,areaCount,cellCount,isNullObject");
|
|
831
|
+
await context.sync();
|
|
832
|
+
return {
|
|
833
|
+
ok: true,
|
|
834
|
+
target,
|
|
835
|
+
data: summarizeRangeAreas(merged),
|
|
836
|
+
warnings: []
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
export async function readRangeComments(request) {
|
|
841
|
+
return unsupportedRangeMetadata(request, "RANGE_COMMENTS_UNSUPPORTED", "Office.js comment-to-range mapping is not enabled in this executor yet.");
|
|
842
|
+
}
|
|
843
|
+
export async function readRangeNotes(request) {
|
|
844
|
+
return unsupportedRangeMetadata(request, "RANGE_NOTES_UNSUPPORTED", "Legacy notes are not exposed through this executor yet.");
|
|
845
|
+
}
|
|
846
|
+
export async function searchRange(request) {
|
|
847
|
+
return Excel.run(async (context) => {
|
|
848
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
849
|
+
const criteria = {};
|
|
850
|
+
if (request.completeMatch !== undefined) {
|
|
851
|
+
criteria.completeMatch = request.completeMatch;
|
|
852
|
+
}
|
|
853
|
+
if (request.matchCase !== undefined) {
|
|
854
|
+
criteria.matchCase = request.matchCase;
|
|
855
|
+
}
|
|
856
|
+
const matches = worksheet.findAllOrNullObject(request.text, criteria);
|
|
857
|
+
matches.load("address,areaCount,cellCount,isNullObject");
|
|
858
|
+
await context.sync();
|
|
859
|
+
return {
|
|
860
|
+
ok: true,
|
|
861
|
+
matches: summarizeRangeAreas(matches)
|
|
862
|
+
};
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
export async function findBlankCells(request) {
|
|
866
|
+
return readSpecialCells(request, "Blanks");
|
|
867
|
+
}
|
|
868
|
+
export async function findFormulaErrors(request) {
|
|
869
|
+
return readSpecialCells(request, "Formulas", "Errors");
|
|
870
|
+
}
|
|
871
|
+
export async function executeBatch(payload) {
|
|
872
|
+
const started = performance.now();
|
|
873
|
+
const counters = {
|
|
874
|
+
syncCount: 0,
|
|
875
|
+
cellsRead: 0,
|
|
876
|
+
cellsWritten: 0,
|
|
877
|
+
rangeCount: payload.compiled.targetFingerprints.length,
|
|
878
|
+
chunkCount: 0
|
|
879
|
+
};
|
|
880
|
+
const warnings = [];
|
|
881
|
+
try {
|
|
882
|
+
if (payload.request.expectedTargetFingerprints?.length) {
|
|
883
|
+
const conflictWarnings = await detectTargetConflicts(payload.request.workbookId, payload.request.expectedTargetFingerprints);
|
|
884
|
+
if (conflictWarnings.length > 0) {
|
|
885
|
+
return {
|
|
886
|
+
ok: false,
|
|
887
|
+
rollbackAvailable: true,
|
|
888
|
+
backups: [],
|
|
889
|
+
warnings: conflictWarnings,
|
|
890
|
+
telemetry: createTelemetry(started, counters, conflictWarnings),
|
|
891
|
+
error: runtimeError("EXTERNAL_CHANGE_DETECTED", "Target ranges changed after preview. Refresh the plan before applying.", {
|
|
892
|
+
retryable: true
|
|
893
|
+
})
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const result = await Excel.run(async (context) => {
|
|
898
|
+
const readOperations = [];
|
|
899
|
+
let formulasChanged = 0;
|
|
900
|
+
let sheetsChanged = 0;
|
|
901
|
+
maybeSuspendExcel(context, payload.compiled.estimatedCellsTouched);
|
|
902
|
+
for (const operation of payload.request.operations) {
|
|
903
|
+
switch (operation.kind) {
|
|
904
|
+
case "range.read_full": {
|
|
905
|
+
const range = getRange(context, operation.target);
|
|
906
|
+
loadSnapshotProperties(range);
|
|
907
|
+
readOperations.push({ operation, range });
|
|
908
|
+
counters.cellsRead += payload.compiled.estimatedCellsTouched;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case "range.write_values": {
|
|
912
|
+
assertMatrixShape(operation.target, operation.values);
|
|
913
|
+
counters.chunkCount += writeMatrixInChunks(context, operation.target, operation.values, "values");
|
|
914
|
+
counters.cellsWritten += matrixCellCount(operation.values);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
case "range.write_formulas": {
|
|
918
|
+
assertMatrixShape(operation.target, operation.formulas);
|
|
919
|
+
counters.chunkCount += writeMatrixInChunks(context, operation.target, operation.formulas, "formulas");
|
|
920
|
+
counters.cellsWritten += matrixCellCount(operation.formulas);
|
|
921
|
+
formulasChanged += matrixCellCount(operation.formulas);
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
case "range.write_number_formats": {
|
|
925
|
+
assertMatrixShape(operation.target, operation.numberFormat);
|
|
926
|
+
counters.chunkCount += writeMatrixInChunks(context, operation.target, operation.numberFormat, "numberFormat");
|
|
927
|
+
counters.cellsWritten += matrixCellCount(operation.numberFormat);
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case "range.write_styles": {
|
|
931
|
+
applyRangeStyle(getRange(context, operation.target), operation.style);
|
|
932
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
case "range.write_hyperlinks":
|
|
936
|
+
case "range.write_comments": {
|
|
937
|
+
warnings.push({
|
|
938
|
+
code: "OPERATION_NOT_SUPPORTED",
|
|
939
|
+
message: `${operation.kind} is defined in the protocol but is not enabled in the Office.js executor yet.`,
|
|
940
|
+
target: operation.target
|
|
941
|
+
});
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
case "range.clear_values_keep_format": {
|
|
945
|
+
getRange(context, operation.target).clear(Excel.ClearApplyTo.contents);
|
|
946
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
case "range.clear":
|
|
950
|
+
case "range.clear_values":
|
|
951
|
+
case "range.clear_formats": {
|
|
952
|
+
const applyTo = operation.kind === "range.clear_values"
|
|
953
|
+
? Excel.ClearApplyTo.contents
|
|
954
|
+
: operation.kind === "range.clear_formats"
|
|
955
|
+
? Excel.ClearApplyTo.formats
|
|
956
|
+
: toClearApplyTo(operation.applyTo ?? "all");
|
|
957
|
+
getRange(context, operation.target).clear(applyTo);
|
|
958
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
case "range.copy": {
|
|
962
|
+
getRange(context, operation.target).copyFrom(getRange(context, operation.source), toRangeCopyType(operation.copyType ?? "all"));
|
|
963
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case "range.move": {
|
|
967
|
+
const source = getRange(context, operation.source);
|
|
968
|
+
const target = getRange(context, operation.target);
|
|
969
|
+
target.copyFrom(source, Excel.RangeCopyType.all);
|
|
970
|
+
source.clear(Excel.ClearApplyTo.all);
|
|
971
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
case "range.insert_rows":
|
|
975
|
+
case "range.insert_columns": {
|
|
976
|
+
getRange(context, operation.target).insert(operation.kind === "range.insert_rows" ? Excel.InsertShiftDirection.down : Excel.InsertShiftDirection.right);
|
|
977
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case "range.delete_rows":
|
|
981
|
+
case "range.delete_columns": {
|
|
982
|
+
getRange(context, operation.target).delete(operation.kind === "range.delete_rows" ? Excel.DeleteShiftDirection.up : Excel.DeleteShiftDirection.left);
|
|
983
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
case "range.autofit_columns": {
|
|
987
|
+
getRange(context, operation.target).format.autofitColumns();
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case "range.autofit_rows": {
|
|
991
|
+
getRange(context, operation.target).format.autofitRows();
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
case "range.merge": {
|
|
995
|
+
getRange(context, operation.target).merge(operation.across ?? false);
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
case "range.unmerge": {
|
|
999
|
+
getRange(context, operation.target).unmerge();
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
case "range.restore_snapshot": {
|
|
1003
|
+
restoreRangeSnapshot(context, operation);
|
|
1004
|
+
counters.cellsWritten += payload.compiled.estimatedCellsTouched;
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
case "workbook.calculate": {
|
|
1008
|
+
context.workbook.application.calculate(operation.calculationType === "recalculate" ? Excel.CalculationType.recalculate : Excel.CalculationType.full);
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case "workbook.save": {
|
|
1012
|
+
context.workbook.save(Excel.SaveBehavior.save);
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
case "sheet.create": {
|
|
1016
|
+
const worksheet = context.workbook.worksheets.add(operation.sheetName);
|
|
1017
|
+
if (operation.activate ?? true) {
|
|
1018
|
+
worksheet.activate();
|
|
1019
|
+
}
|
|
1020
|
+
sheetsChanged += 1;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
case "sheet.copy": {
|
|
1024
|
+
const sourceSheet = context.workbook.worksheets.getItem(operation.sourceSheetName);
|
|
1025
|
+
const relativeSheet = operation.relativeToSheetName
|
|
1026
|
+
? context.workbook.worksheets.getItem(operation.relativeToSheetName)
|
|
1027
|
+
: sourceSheet;
|
|
1028
|
+
const copiedSheet = sourceSheet.copy(toWorksheetPositionType(operation.position ?? "after"), relativeSheet);
|
|
1029
|
+
copiedSheet.name = operation.newSheetName;
|
|
1030
|
+
if (operation.activate ?? true) {
|
|
1031
|
+
copiedSheet.activate();
|
|
1032
|
+
}
|
|
1033
|
+
sheetsChanged += 1;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
case "sheet.rename": {
|
|
1037
|
+
context.workbook.worksheets.getItem(operation.sheetName).name = operation.newSheetName;
|
|
1038
|
+
sheetsChanged += 1;
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
case "sheet.delete": {
|
|
1042
|
+
context.workbook.worksheets.getItem(operation.sheetName).delete();
|
|
1043
|
+
sheetsChanged += 1;
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
case "sheet.move": {
|
|
1047
|
+
warnings.push({
|
|
1048
|
+
code: "OPERATION_NOT_SUPPORTED",
|
|
1049
|
+
message: "sheet.move is defined in the protocol but is not enabled in the Office.js executor yet."
|
|
1050
|
+
});
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
case "sheet.hide": {
|
|
1054
|
+
context.workbook.worksheets.getItem(operation.sheetName).visibility = Excel.SheetVisibility.hidden;
|
|
1055
|
+
sheetsChanged += 1;
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
case "sheet.unhide": {
|
|
1059
|
+
context.workbook.worksheets.getItem(operation.sheetName).visibility = Excel.SheetVisibility.visible;
|
|
1060
|
+
sheetsChanged += 1;
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
case "sheet.protect": {
|
|
1064
|
+
context.workbook.worksheets.getItem(operation.sheetName).protection.protect(undefined, operation.password);
|
|
1065
|
+
sheetsChanged += 1;
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
case "sheet.unprotect": {
|
|
1069
|
+
context.workbook.worksheets.getItem(operation.sheetName).protection.unprotect(operation.password);
|
|
1070
|
+
sheetsChanged += 1;
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case "sheet.clear": {
|
|
1074
|
+
const usedRange = context.workbook.worksheets.getItem(operation.sheetName).getUsedRangeOrNullObject();
|
|
1075
|
+
usedRange.clear(toClearApplyTo(operation.applyTo ?? "all"));
|
|
1076
|
+
sheetsChanged += 1;
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case "sheet.set_tab_color": {
|
|
1080
|
+
context.workbook.worksheets.getItem(operation.sheetName).tabColor = operation.color;
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
case "template.create_sheet_from_template": {
|
|
1084
|
+
const warning = applyTemplateSheetOperation(context, operation, payload.templateSources ?? []);
|
|
1085
|
+
if (warning) {
|
|
1086
|
+
warnings.push(warning);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
sheetsChanged += 1;
|
|
1090
|
+
}
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
await context.sync();
|
|
1096
|
+
counters.syncCount += 1;
|
|
1097
|
+
const readData = readOperations.map(({ operation, range }) => ({
|
|
1098
|
+
operationId: operation.operationId,
|
|
1099
|
+
snapshot: materializeSnapshot("target" in operation ? operation.target : payload.compiled.targetFingerprints[0].range, range)
|
|
1100
|
+
}));
|
|
1101
|
+
const changedRanges = payload.compiled.targetFingerprints.map((fingerprint) => fingerprint.range);
|
|
1102
|
+
const diffSummary = {
|
|
1103
|
+
title: "Excel batch applied",
|
|
1104
|
+
changedRanges,
|
|
1105
|
+
cellsChanged: payload.compiled.estimatedCellsTouched,
|
|
1106
|
+
formulasChanged,
|
|
1107
|
+
stylesChanged: 0,
|
|
1108
|
+
tablesChanged: 0,
|
|
1109
|
+
sheetsChanged,
|
|
1110
|
+
destructiveLevel: payload.compiled.destructiveLevel
|
|
1111
|
+
};
|
|
1112
|
+
return { diffSummary, readData };
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
ok: warnings.every((warning) => warning.code !== "TEMPLATE_SOURCE_MISSING"),
|
|
1116
|
+
diffSummary: result.diffSummary,
|
|
1117
|
+
data: result.readData,
|
|
1118
|
+
rollbackAvailable: payload.compiled.requiredBackups.length > 0,
|
|
1119
|
+
backups: [],
|
|
1120
|
+
warnings,
|
|
1121
|
+
telemetry: createTelemetry(started, counters, warnings)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
catch (error) {
|
|
1125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1126
|
+
return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
rollbackAvailable: payload.compiled.requiredBackups.length > 0,
|
|
1129
|
+
backups: [],
|
|
1130
|
+
warnings,
|
|
1131
|
+
telemetry: createTelemetry(started, counters, warnings),
|
|
1132
|
+
error: runtimeError("OPERATION_FAILED", message, { retryable: false })
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
export async function captureTemplate(request) {
|
|
1137
|
+
const captured = await captureSheetFingerprint({
|
|
1138
|
+
workbookId: request.workbookId,
|
|
1139
|
+
sheetName: request.sourceSheetName,
|
|
1140
|
+
dataRegions: request.dataRegions
|
|
1141
|
+
});
|
|
1142
|
+
return {
|
|
1143
|
+
sourceSheetName: captured.sourceSheetName,
|
|
1144
|
+
dataRegions: captured.dataRegions,
|
|
1145
|
+
fingerprintPayload: captured.fingerprintPayload
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
export async function captureSheetFingerprint(request) {
|
|
1149
|
+
return Excel.run(async (context) => {
|
|
1150
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
1151
|
+
const usedRange = worksheet.getUsedRangeOrNullObject();
|
|
1152
|
+
const tables = worksheet.tables;
|
|
1153
|
+
worksheet.load("name, position, visibility");
|
|
1154
|
+
usedRange.load("address, rowCount, columnCount, values, formulas, numberFormat");
|
|
1155
|
+
usedRange.format.load("rowHeight, columnWidth, horizontalAlignment, verticalAlignment");
|
|
1156
|
+
usedRange.format.fill.load("color");
|
|
1157
|
+
usedRange.format.font.load("name, size, color, bold, italic");
|
|
1158
|
+
tables.load("items/name");
|
|
1159
|
+
await context.sync();
|
|
1160
|
+
const rangePayload = usedRange.isNullObject
|
|
1161
|
+
? null
|
|
1162
|
+
: {
|
|
1163
|
+
address: usedRange.address,
|
|
1164
|
+
rowCount: usedRange.rowCount,
|
|
1165
|
+
columnCount: usedRange.columnCount,
|
|
1166
|
+
values: usedRange.values,
|
|
1167
|
+
formulas: usedRange.formulas,
|
|
1168
|
+
numberFormat: usedRange.numberFormat
|
|
1169
|
+
};
|
|
1170
|
+
const stylePayload = usedRange.isNullObject
|
|
1171
|
+
? null
|
|
1172
|
+
: {
|
|
1173
|
+
fillColor: usedRange.format.fill.color,
|
|
1174
|
+
fontName: usedRange.format.font.name,
|
|
1175
|
+
fontSize: usedRange.format.font.size,
|
|
1176
|
+
fontColor: usedRange.format.font.color,
|
|
1177
|
+
fontBold: usedRange.format.font.bold,
|
|
1178
|
+
fontItalic: usedRange.format.font.italic,
|
|
1179
|
+
horizontalAlignment: usedRange.format.horizontalAlignment,
|
|
1180
|
+
verticalAlignment: usedRange.format.verticalAlignment,
|
|
1181
|
+
rowHeight: usedRange.format.rowHeight,
|
|
1182
|
+
columnWidth: usedRange.format.columnWidth
|
|
1183
|
+
};
|
|
1184
|
+
return {
|
|
1185
|
+
sheetName: worksheet.name,
|
|
1186
|
+
sourceSheetName: worksheet.name,
|
|
1187
|
+
dataRegions: request.dataRegions ?? [],
|
|
1188
|
+
fingerprintPayload: {
|
|
1189
|
+
structure: {
|
|
1190
|
+
sheetName: worksheet.name,
|
|
1191
|
+
position: worksheet.position,
|
|
1192
|
+
visibility: worksheet.visibility,
|
|
1193
|
+
usedRange: rangePayload
|
|
1194
|
+
? {
|
|
1195
|
+
address: rangePayload.address,
|
|
1196
|
+
rowCount: rangePayload.rowCount,
|
|
1197
|
+
columnCount: rangePayload.columnCount
|
|
1198
|
+
}
|
|
1199
|
+
: null,
|
|
1200
|
+
dataRegions: request.dataRegions ?? []
|
|
1201
|
+
},
|
|
1202
|
+
formulas: rangePayload?.formulas ?? null,
|
|
1203
|
+
styles: {
|
|
1204
|
+
usedRange: stylePayload,
|
|
1205
|
+
numberFormat: rangePayload?.numberFormat ?? null
|
|
1206
|
+
},
|
|
1207
|
+
filters: {
|
|
1208
|
+
note: "Filter capture will be expanded with table/filter-specific APIs."
|
|
1209
|
+
},
|
|
1210
|
+
tables: tables.items.map((table) => ({ name: table.name })),
|
|
1211
|
+
printLayout: {
|
|
1212
|
+
note: "Print layout capture will be expanded with page layout APIs."
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
export async function captureStyleFingerprint(request) {
|
|
1219
|
+
return Excel.run(async (context) => {
|
|
1220
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
1221
|
+
const range = request.address ? worksheet.getRange(stripSheetName(request.address)) : worksheet.getUsedRangeOrNullObject();
|
|
1222
|
+
range.load("address, rowCount, columnCount, numberFormat");
|
|
1223
|
+
range.format.load("rowHeight, columnWidth, horizontalAlignment, verticalAlignment, wrapText");
|
|
1224
|
+
range.format.fill.load("color");
|
|
1225
|
+
range.format.font.load("name, size, color, bold, italic, underline");
|
|
1226
|
+
range.format.borders.load("items/sideIndex,items/style,items/color,items/weight");
|
|
1227
|
+
await context.sync();
|
|
1228
|
+
if ("isNullObject" in range && range.isNullObject) {
|
|
1229
|
+
return {
|
|
1230
|
+
workbookId: request.workbookId,
|
|
1231
|
+
sheetName: request.sheetName,
|
|
1232
|
+
address: "",
|
|
1233
|
+
capturedAt: new Date().toISOString(),
|
|
1234
|
+
rowCount: 0,
|
|
1235
|
+
columnCount: 0,
|
|
1236
|
+
truncated: false,
|
|
1237
|
+
dimensions: {},
|
|
1238
|
+
warnings: [
|
|
1239
|
+
{
|
|
1240
|
+
code: "EMPTY_USED_RANGE",
|
|
1241
|
+
message: `Sheet ${request.sheetName} has no used range to fingerprint.`
|
|
1242
|
+
}
|
|
1243
|
+
]
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const columnRanges = [];
|
|
1247
|
+
const rowRanges = [];
|
|
1248
|
+
for (let columnIndex = 0; columnIndex < range.columnCount; columnIndex += 1) {
|
|
1249
|
+
const column = range.getColumn(columnIndex);
|
|
1250
|
+
column.format.load("columnWidth");
|
|
1251
|
+
columnRanges.push(column);
|
|
1252
|
+
}
|
|
1253
|
+
for (let rowIndex = 0; rowIndex < range.rowCount; rowIndex += 1) {
|
|
1254
|
+
const row = range.getRow(rowIndex);
|
|
1255
|
+
row.format.load("rowHeight");
|
|
1256
|
+
rowRanges.push(row);
|
|
1257
|
+
}
|
|
1258
|
+
const sampleLimit = request.maxCellSamples ?? 500;
|
|
1259
|
+
const cellCount = range.rowCount * range.columnCount;
|
|
1260
|
+
const sampledCells = [];
|
|
1261
|
+
if (cellCount <= sampleLimit) {
|
|
1262
|
+
for (let rowIndex = 0; rowIndex < range.rowCount; rowIndex += 1) {
|
|
1263
|
+
for (let columnIndex = 0; columnIndex < range.columnCount; columnIndex += 1) {
|
|
1264
|
+
const cell = range.getCell(rowIndex, columnIndex);
|
|
1265
|
+
cell.load("numberFormat");
|
|
1266
|
+
cell.format.load("horizontalAlignment, verticalAlignment, wrapText");
|
|
1267
|
+
cell.format.fill.load("color");
|
|
1268
|
+
cell.format.font.load("name, size, color, bold, italic, underline");
|
|
1269
|
+
sampledCells.push({ rowIndex, columnIndex, cell });
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
await context.sync();
|
|
1274
|
+
const cellStyles = sampledCells.map(({ rowIndex, columnIndex, cell }) => ({
|
|
1275
|
+
rowIndex,
|
|
1276
|
+
columnIndex,
|
|
1277
|
+
fillColor: optionalValue(cell.format.fill.color),
|
|
1278
|
+
fontName: optionalValue(cell.format.font.name),
|
|
1279
|
+
fontSize: optionalValue(cell.format.font.size),
|
|
1280
|
+
fontColor: optionalValue(cell.format.font.color),
|
|
1281
|
+
fontBold: optionalValue(cell.format.font.bold),
|
|
1282
|
+
fontItalic: optionalValue(cell.format.font.italic),
|
|
1283
|
+
fontUnderline: optionalValue(String(cell.format.font.underline)),
|
|
1284
|
+
horizontalAlignment: optionalValue(String(cell.format.horizontalAlignment)),
|
|
1285
|
+
verticalAlignment: optionalValue(String(cell.format.verticalAlignment)),
|
|
1286
|
+
wrapText: optionalValue(cell.format.wrapText),
|
|
1287
|
+
numberFormat: cell.numberFormat[0]?.[0]
|
|
1288
|
+
}));
|
|
1289
|
+
const warnings = [];
|
|
1290
|
+
if (cellCount > sampleLimit) {
|
|
1291
|
+
warnings.push({
|
|
1292
|
+
code: "STYLE_SAMPLE_TRUNCATED",
|
|
1293
|
+
message: `Cell-level style sampling skipped for ${cellCount} cells; increase maxCellSamples or pass a smaller address.`,
|
|
1294
|
+
details: { cellCount, sampleLimit }
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
workbookId: request.workbookId,
|
|
1299
|
+
sheetName: request.sheetName,
|
|
1300
|
+
address: stripSheetName(range.address),
|
|
1301
|
+
capturedAt: new Date().toISOString(),
|
|
1302
|
+
rowCount: range.rowCount,
|
|
1303
|
+
columnCount: range.columnCount,
|
|
1304
|
+
truncated: cellCount > sampleLimit,
|
|
1305
|
+
dimensions: {
|
|
1306
|
+
columnWidths: columnRanges.map((column, columnIndex) => ({ columnIndex, width: optionalValue(column.format.columnWidth) })),
|
|
1307
|
+
rowHeights: rowRanges.map((row, rowIndex) => ({ rowIndex, height: optionalValue(row.format.rowHeight) })),
|
|
1308
|
+
fills: {
|
|
1309
|
+
rangeColor: optionalValue(range.format.fill.color),
|
|
1310
|
+
cells: cellStyles.map(({ rowIndex, columnIndex, fillColor }) => ({ rowIndex, columnIndex, fillColor }))
|
|
1311
|
+
},
|
|
1312
|
+
fonts: {
|
|
1313
|
+
range: {
|
|
1314
|
+
name: optionalValue(range.format.font.name),
|
|
1315
|
+
size: optionalValue(range.format.font.size),
|
|
1316
|
+
color: optionalValue(range.format.font.color),
|
|
1317
|
+
bold: optionalValue(range.format.font.bold),
|
|
1318
|
+
italic: optionalValue(range.format.font.italic),
|
|
1319
|
+
underline: optionalValue(String(range.format.font.underline))
|
|
1320
|
+
},
|
|
1321
|
+
cells: cellStyles.map(({ rowIndex, columnIndex, fontName, fontSize, fontColor, fontBold, fontItalic, fontUnderline }) => ({
|
|
1322
|
+
rowIndex,
|
|
1323
|
+
columnIndex,
|
|
1324
|
+
fontName,
|
|
1325
|
+
fontSize,
|
|
1326
|
+
fontColor,
|
|
1327
|
+
fontBold,
|
|
1328
|
+
fontItalic,
|
|
1329
|
+
fontUnderline
|
|
1330
|
+
}))
|
|
1331
|
+
},
|
|
1332
|
+
alignment: {
|
|
1333
|
+
range: {
|
|
1334
|
+
horizontalAlignment: optionalValue(String(range.format.horizontalAlignment)),
|
|
1335
|
+
verticalAlignment: optionalValue(String(range.format.verticalAlignment)),
|
|
1336
|
+
wrapText: optionalValue(range.format.wrapText)
|
|
1337
|
+
},
|
|
1338
|
+
cells: cellStyles.map(({ rowIndex, columnIndex, horizontalAlignment, verticalAlignment, wrapText }) => ({
|
|
1339
|
+
rowIndex,
|
|
1340
|
+
columnIndex,
|
|
1341
|
+
horizontalAlignment,
|
|
1342
|
+
verticalAlignment,
|
|
1343
|
+
wrapText
|
|
1344
|
+
}))
|
|
1345
|
+
},
|
|
1346
|
+
numberFormats: {
|
|
1347
|
+
matrix: range.numberFormat,
|
|
1348
|
+
cells: cellStyles.map(({ rowIndex, columnIndex, numberFormat }) => ({ rowIndex, columnIndex, numberFormat }))
|
|
1349
|
+
},
|
|
1350
|
+
borders: range.format.borders.items ?? [],
|
|
1351
|
+
conditionalFormatting: {
|
|
1352
|
+
note: "Use excel.range.read_conditional_formatting for detailed rule inspection."
|
|
1353
|
+
},
|
|
1354
|
+
dataValidation: {
|
|
1355
|
+
note: "Use excel.range.read_data_validation for detailed rule inspection."
|
|
1356
|
+
},
|
|
1357
|
+
freezePanes: {
|
|
1358
|
+
note: "Office.js freeze pane capture is tracked as a layout capability."
|
|
1359
|
+
},
|
|
1360
|
+
printSettings: {
|
|
1361
|
+
note: "Office.js print setting capture is tracked as a layout capability."
|
|
1362
|
+
},
|
|
1363
|
+
pageLayout: {
|
|
1364
|
+
note: "Office.js page layout capture is tracked as a layout capability."
|
|
1365
|
+
},
|
|
1366
|
+
hiddenRowsColumns: {
|
|
1367
|
+
note: "Hidden row/column capture is tracked as a layout capability."
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
warnings
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
export async function copyStyleDimensions(request) {
|
|
1375
|
+
return Excel.run(async (context) => {
|
|
1376
|
+
const sourceSheet = context.workbook.worksheets.getItem(request.sourceSheetName);
|
|
1377
|
+
const targetSheet = context.workbook.worksheets.getItem(request.targetSheetName);
|
|
1378
|
+
const sourceRange = request.sourceAddress ? sourceSheet.getRange(stripSheetName(request.sourceAddress)) : sourceSheet.getUsedRangeOrNullObject();
|
|
1379
|
+
sourceRange.load("address,rowCount,columnCount");
|
|
1380
|
+
await context.sync();
|
|
1381
|
+
if ("isNullObject" in sourceRange && sourceRange.isNullObject) {
|
|
1382
|
+
return {
|
|
1383
|
+
ok: false,
|
|
1384
|
+
copied: [],
|
|
1385
|
+
warnings: [
|
|
1386
|
+
{
|
|
1387
|
+
code: "EMPTY_SOURCE_RANGE",
|
|
1388
|
+
message: `Sheet ${request.sourceSheetName} has no used range to copy styles from.`
|
|
1389
|
+
}
|
|
1390
|
+
]
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const targetAddress = request.targetAddress ?? stripSheetName(sourceRange.address);
|
|
1394
|
+
const targetRange = targetSheet.getRange(stripSheetName(targetAddress));
|
|
1395
|
+
const copied = [];
|
|
1396
|
+
const warnings = [];
|
|
1397
|
+
const dimensions = new Set(request.dimensions);
|
|
1398
|
+
const copyAll = request.dimensions.length === 0;
|
|
1399
|
+
const formatDimensions = ["borders", "fills", "fonts", "alignment", "numberFormats", "conditionalFormatting", "dataValidation"];
|
|
1400
|
+
if (copyAll || formatDimensions.some((dimension) => dimensions.has(dimension))) {
|
|
1401
|
+
targetRange.copyFrom(sourceRange, Excel.RangeCopyType.formats);
|
|
1402
|
+
copied.push(...formatDimensions.filter((dimension) => copyAll || dimensions.has(dimension)));
|
|
1403
|
+
}
|
|
1404
|
+
const copyColumnWidths = copyAll || dimensions.has("columnWidths");
|
|
1405
|
+
const copyRowHeights = copyAll || dimensions.has("rowHeights");
|
|
1406
|
+
const sourceColumns = [];
|
|
1407
|
+
const targetColumns = [];
|
|
1408
|
+
const sourceRows = [];
|
|
1409
|
+
const targetRows = [];
|
|
1410
|
+
if (copyColumnWidths) {
|
|
1411
|
+
for (let columnIndex = 0; columnIndex < sourceRange.columnCount; columnIndex += 1) {
|
|
1412
|
+
const sourceColumn = sourceRange.getColumn(columnIndex);
|
|
1413
|
+
sourceColumn.format.load("columnWidth");
|
|
1414
|
+
sourceColumns.push(sourceColumn);
|
|
1415
|
+
targetColumns.push(targetRange.getColumn(columnIndex));
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (copyRowHeights) {
|
|
1419
|
+
for (let rowIndex = 0; rowIndex < sourceRange.rowCount; rowIndex += 1) {
|
|
1420
|
+
const sourceRow = sourceRange.getRow(rowIndex);
|
|
1421
|
+
sourceRow.format.load("rowHeight");
|
|
1422
|
+
sourceRows.push(sourceRow);
|
|
1423
|
+
targetRows.push(targetRange.getRow(rowIndex));
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (copyColumnWidths || copyRowHeights) {
|
|
1427
|
+
await context.sync();
|
|
1428
|
+
}
|
|
1429
|
+
if (copyColumnWidths) {
|
|
1430
|
+
sourceColumns.forEach((sourceColumn, index) => {
|
|
1431
|
+
targetColumns[index].format.columnWidth = sourceColumn.format.columnWidth;
|
|
1432
|
+
});
|
|
1433
|
+
copied.push("columnWidths");
|
|
1434
|
+
}
|
|
1435
|
+
if (copyRowHeights) {
|
|
1436
|
+
sourceRows.forEach((sourceRow, index) => {
|
|
1437
|
+
targetRows[index].format.rowHeight = sourceRow.format.rowHeight;
|
|
1438
|
+
});
|
|
1439
|
+
copied.push("rowHeights");
|
|
1440
|
+
}
|
|
1441
|
+
for (const unsupported of ["freezePanes", "printSettings", "pageLayout", "hiddenRowsColumns"]) {
|
|
1442
|
+
if (copyAll || dimensions.has(unsupported)) {
|
|
1443
|
+
warnings.push({
|
|
1444
|
+
code: "STYLE_LAYOUT_DIMENSION_UNAVAILABLE",
|
|
1445
|
+
message: `${unsupported} is tracked in fingerprints but is not safely replayed through the current Office.js path.`,
|
|
1446
|
+
details: { dimension: unsupported }
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
await context.sync();
|
|
1451
|
+
return { ok: warnings.length === 0 || copied.length > 0, copied: [...new Set(copied)], warnings };
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
export async function readFormulaPatterns(request) {
|
|
1455
|
+
return Excel.run(async (context) => {
|
|
1456
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
1457
|
+
const range = request.address ? worksheet.getRange(stripSheetName(request.address)) : worksheet.getUsedRangeOrNullObject();
|
|
1458
|
+
range.load("address,rowCount,columnCount,formulas,formulasR1C1");
|
|
1459
|
+
await context.sync();
|
|
1460
|
+
if ("isNullObject" in range && range.isNullObject) {
|
|
1461
|
+
return {
|
|
1462
|
+
workbookId: request.workbookId,
|
|
1463
|
+
sheetName: request.sheetName,
|
|
1464
|
+
address: "",
|
|
1465
|
+
capturedAt: new Date().toISOString(),
|
|
1466
|
+
rowCount: 0,
|
|
1467
|
+
columnCount: 0,
|
|
1468
|
+
formulaCount: 0,
|
|
1469
|
+
formulas: [],
|
|
1470
|
+
patternMatrix: [],
|
|
1471
|
+
patterns: [],
|
|
1472
|
+
cells: [],
|
|
1473
|
+
warnings: [
|
|
1474
|
+
{
|
|
1475
|
+
code: "EMPTY_USED_RANGE",
|
|
1476
|
+
message: `Sheet ${request.sheetName} has no used range to inspect for formulas.`
|
|
1477
|
+
}
|
|
1478
|
+
]
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
const formulas = normalizeFormulaMatrix(range.formulas);
|
|
1482
|
+
const formulasR1C1 = normalizeFormulaMatrix(range.formulasR1C1 ?? []);
|
|
1483
|
+
const patternMatrix = [];
|
|
1484
|
+
const patternMap = new Map();
|
|
1485
|
+
const cells = [];
|
|
1486
|
+
for (let rowIndex = 0; rowIndex < range.rowCount; rowIndex += 1) {
|
|
1487
|
+
const row = [];
|
|
1488
|
+
for (let columnIndex = 0; columnIndex < range.columnCount; columnIndex += 1) {
|
|
1489
|
+
const formula = formulas[rowIndex]?.[columnIndex] ?? null;
|
|
1490
|
+
const formulaR1C1 = formulasR1C1[rowIndex]?.[columnIndex] ?? formula;
|
|
1491
|
+
if (!formula || !formula.startsWith("=")) {
|
|
1492
|
+
row.push(null);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
const patternHash = hashStable(formulaR1C1 ?? formula);
|
|
1496
|
+
row.push(patternHash);
|
|
1497
|
+
const pattern = patternMap.get(patternHash) ?? {
|
|
1498
|
+
patternHash,
|
|
1499
|
+
formulaR1C1: formulaR1C1 ?? formula,
|
|
1500
|
+
count: 0,
|
|
1501
|
+
cells: []
|
|
1502
|
+
};
|
|
1503
|
+
pattern.count += 1;
|
|
1504
|
+
pattern.cells.push({ rowIndex, columnIndex });
|
|
1505
|
+
patternMap.set(patternHash, pattern);
|
|
1506
|
+
cells.push({
|
|
1507
|
+
rowIndex,
|
|
1508
|
+
columnIndex,
|
|
1509
|
+
formula,
|
|
1510
|
+
...(formulaR1C1 !== undefined && formulaR1C1 !== null ? { formulaR1C1 } : {}),
|
|
1511
|
+
patternHash
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
patternMatrix.push(row);
|
|
1515
|
+
}
|
|
1516
|
+
return {
|
|
1517
|
+
workbookId: request.workbookId,
|
|
1518
|
+
sheetName: request.sheetName,
|
|
1519
|
+
address: stripSheetName(range.address),
|
|
1520
|
+
capturedAt: new Date().toISOString(),
|
|
1521
|
+
rowCount: range.rowCount,
|
|
1522
|
+
columnCount: range.columnCount,
|
|
1523
|
+
formulaCount: cells.length,
|
|
1524
|
+
formulas,
|
|
1525
|
+
formulasR1C1,
|
|
1526
|
+
patternMatrix,
|
|
1527
|
+
patterns: [...patternMap.values()],
|
|
1528
|
+
cells,
|
|
1529
|
+
warnings: []
|
|
1530
|
+
};
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
export async function copyFormulaPatterns(request) {
|
|
1534
|
+
return Excel.run(async (context) => {
|
|
1535
|
+
const sourceSheet = context.workbook.worksheets.getItem(request.sourceSheetName);
|
|
1536
|
+
const targetSheet = context.workbook.worksheets.getItem(request.targetSheetName);
|
|
1537
|
+
const sourceRange = request.sourceAddress ? sourceSheet.getRange(stripSheetName(request.sourceAddress)) : sourceSheet.getUsedRangeOrNullObject();
|
|
1538
|
+
sourceRange.load("address,rowCount,columnCount");
|
|
1539
|
+
await context.sync();
|
|
1540
|
+
if ("isNullObject" in sourceRange && sourceRange.isNullObject) {
|
|
1541
|
+
return {
|
|
1542
|
+
ok: false,
|
|
1543
|
+
formulasChanged: 0,
|
|
1544
|
+
warnings: [
|
|
1545
|
+
{
|
|
1546
|
+
code: "EMPTY_SOURCE_RANGE",
|
|
1547
|
+
message: `Sheet ${request.sourceSheetName} has no formulas to copy.`
|
|
1548
|
+
}
|
|
1549
|
+
]
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
const targetRange = targetSheet.getRange(stripSheetName(request.targetAddress ?? sourceRange.address));
|
|
1553
|
+
targetRange.copyFrom(sourceRange, Excel.RangeCopyType.formulas);
|
|
1554
|
+
await context.sync();
|
|
1555
|
+
return {
|
|
1556
|
+
ok: true,
|
|
1557
|
+
formulasChanged: sourceRange.rowCount * sourceRange.columnCount,
|
|
1558
|
+
warnings: []
|
|
1559
|
+
};
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
export async function fillFormulaPattern(request) {
|
|
1563
|
+
return Excel.run(async (context) => {
|
|
1564
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
1565
|
+
const sourceRange = worksheet.getRange(stripSheetName(request.sourceAddress));
|
|
1566
|
+
const targetRange = worksheet.getRange(stripSheetName(request.targetAddress));
|
|
1567
|
+
sourceRange.load("formulasR1C1,rowCount,columnCount");
|
|
1568
|
+
targetRange.load("rowCount,columnCount");
|
|
1569
|
+
await context.sync();
|
|
1570
|
+
const sourceFormulas = normalizeFormulaMatrix(sourceRange.formulasR1C1 ?? []);
|
|
1571
|
+
const warnings = [];
|
|
1572
|
+
if (!matrixHasFormula(sourceFormulas)) {
|
|
1573
|
+
warnings.push({
|
|
1574
|
+
code: "NO_SOURCE_FORMULAS",
|
|
1575
|
+
message: `${request.sheetName}!${request.sourceAddress} does not contain formulas to fill.`
|
|
1576
|
+
});
|
|
1577
|
+
return { ok: false, formulasChanged: 0, warnings };
|
|
1578
|
+
}
|
|
1579
|
+
const formulas = [];
|
|
1580
|
+
for (let rowIndex = 0; rowIndex < targetRange.rowCount; rowIndex += 1) {
|
|
1581
|
+
const row = [];
|
|
1582
|
+
for (let columnIndex = 0; columnIndex < targetRange.columnCount; columnIndex += 1) {
|
|
1583
|
+
const sourceRow = request.direction === "down" ? Math.min(rowIndex, sourceFormulas.length - 1) : rowIndex % sourceFormulas.length;
|
|
1584
|
+
const sourceColumn = request.direction === "right" ? Math.min(columnIndex, (sourceFormulas[0]?.length ?? 1) - 1) : columnIndex % (sourceFormulas[0]?.length ?? 1);
|
|
1585
|
+
row.push(sourceFormulas[sourceRow]?.[sourceColumn] ?? sourceFormulas[0]?.[0] ?? null);
|
|
1586
|
+
}
|
|
1587
|
+
formulas.push(row);
|
|
1588
|
+
}
|
|
1589
|
+
targetRange.formulasR1C1 = formulas;
|
|
1590
|
+
await context.sync();
|
|
1591
|
+
return {
|
|
1592
|
+
ok: true,
|
|
1593
|
+
formulasChanged: targetRange.rowCount * targetRange.columnCount,
|
|
1594
|
+
warnings
|
|
1595
|
+
};
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
export async function convertFormulasToValues(request) {
|
|
1599
|
+
return Excel.run(async (context) => {
|
|
1600
|
+
const worksheet = context.workbook.worksheets.getItem(request.sheetName);
|
|
1601
|
+
const range = request.address ? worksheet.getRange(stripSheetName(request.address)) : worksheet.getUsedRangeOrNullObject();
|
|
1602
|
+
range.load("values,formulas,rowCount,columnCount");
|
|
1603
|
+
await context.sync();
|
|
1604
|
+
if ("isNullObject" in range && range.isNullObject) {
|
|
1605
|
+
return {
|
|
1606
|
+
ok: false,
|
|
1607
|
+
formulasChanged: 0,
|
|
1608
|
+
warnings: [{ code: "EMPTY_USED_RANGE", message: `Sheet ${request.sheetName} has no used range.` }]
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const formulaCount = countFormulas(normalizeFormulaMatrix(range.formulas));
|
|
1612
|
+
range.values = range.values;
|
|
1613
|
+
await context.sync();
|
|
1614
|
+
return { ok: true, formulasChanged: formulaCount, warnings: [] };
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
export async function repairTemplateConsistency(request) {
|
|
1618
|
+
return Excel.run(async (context) => {
|
|
1619
|
+
const sourceSheet = context.workbook.worksheets.getItem(request.sourceSheetName);
|
|
1620
|
+
const targetSheet = context.workbook.worksheets.getItem(request.targetSheetName);
|
|
1621
|
+
const sourceUsedRange = sourceSheet.getUsedRangeOrNullObject();
|
|
1622
|
+
sourceUsedRange.load("address");
|
|
1623
|
+
await context.sync();
|
|
1624
|
+
const repaired = [];
|
|
1625
|
+
if (!sourceUsedRange.isNullObject && request.repair.includes("layout")) {
|
|
1626
|
+
const targetRange = targetSheet.getRange(stripSheetName(sourceUsedRange.address));
|
|
1627
|
+
targetRange.copyFrom(sourceUsedRange, Excel.RangeCopyType.all);
|
|
1628
|
+
repaired.push("layout");
|
|
1629
|
+
}
|
|
1630
|
+
else if (!sourceUsedRange.isNullObject) {
|
|
1631
|
+
const targetRange = targetSheet.getRange(stripSheetName(sourceUsedRange.address));
|
|
1632
|
+
if (request.repair.includes("styles")) {
|
|
1633
|
+
targetRange.copyFrom(sourceUsedRange, Excel.RangeCopyType.formats);
|
|
1634
|
+
repaired.push("styles");
|
|
1635
|
+
}
|
|
1636
|
+
if (request.repair.includes("formulas")) {
|
|
1637
|
+
targetRange.copyFrom(sourceUsedRange, Excel.RangeCopyType.formulas);
|
|
1638
|
+
repaired.push("formulas");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (request.repair.includes("dataRegions")) {
|
|
1642
|
+
for (const dataRegion of request.dataRegions) {
|
|
1643
|
+
targetSheet.getRange(stripSheetName(dataRegion)).clear(Excel.ClearApplyTo.contents);
|
|
1644
|
+
}
|
|
1645
|
+
repaired.push("dataRegions");
|
|
1646
|
+
}
|
|
1647
|
+
await context.sync();
|
|
1648
|
+
return { ok: true, repaired };
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
async function detectTargetConflicts(workbookId, expected) {
|
|
1652
|
+
const current = await snapshotRanges(workbookId, expected.map((fingerprint) => fingerprint.range));
|
|
1653
|
+
const warnings = [];
|
|
1654
|
+
for (const expectedFingerprint of expected) {
|
|
1655
|
+
const currentFingerprint = current.rangeSnapshots.find((snapshot) => snapshot.fingerprint.range.sheetName === expectedFingerprint.range.sheetName &&
|
|
1656
|
+
snapshot.fingerprint.range.address === expectedFingerprint.range.address)?.fingerprint;
|
|
1657
|
+
if (!currentFingerprint || currentFingerprint.hash !== expectedFingerprint.hash) {
|
|
1658
|
+
warnings.push({
|
|
1659
|
+
code: "TARGET_REGION_CHANGED",
|
|
1660
|
+
message: `Target changed after preview: ${expectedFingerprint.range.sheetName}!${expectedFingerprint.range.address}`,
|
|
1661
|
+
target: expectedFingerprint.range
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return warnings;
|
|
1666
|
+
}
|
|
1667
|
+
async function readRangeMetadata(request, load, materialize) {
|
|
1668
|
+
return Excel.run(async (context) => {
|
|
1669
|
+
const target = targetFromMetadataRequest(request);
|
|
1670
|
+
const range = getRange(context, target);
|
|
1671
|
+
load(range);
|
|
1672
|
+
await context.sync();
|
|
1673
|
+
return {
|
|
1674
|
+
ok: true,
|
|
1675
|
+
target,
|
|
1676
|
+
data: materialize(range),
|
|
1677
|
+
warnings: []
|
|
1678
|
+
};
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
async function readSpecialCells(request, cellType, cellValueType) {
|
|
1682
|
+
return Excel.run(async (context) => {
|
|
1683
|
+
const target = targetFromMetadataRequest(request);
|
|
1684
|
+
const range = getRange(context, target);
|
|
1685
|
+
const areas = range.getSpecialCellsOrNullObject(cellType, cellValueType);
|
|
1686
|
+
areas.load("address,areaCount,cellCount,isNullObject");
|
|
1687
|
+
await context.sync();
|
|
1688
|
+
return {
|
|
1689
|
+
ok: true,
|
|
1690
|
+
target,
|
|
1691
|
+
data: summarizeRangeAreas(areas),
|
|
1692
|
+
warnings: []
|
|
1693
|
+
};
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
function unsupportedRangeMetadata(request, code, message) {
|
|
1697
|
+
return {
|
|
1698
|
+
ok: false,
|
|
1699
|
+
target: targetFromMetadataRequest(request),
|
|
1700
|
+
warnings: [
|
|
1701
|
+
{
|
|
1702
|
+
code,
|
|
1703
|
+
message,
|
|
1704
|
+
target: targetFromMetadataRequest(request)
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function targetFromMetadataRequest(request) {
|
|
1710
|
+
return {
|
|
1711
|
+
workbookId: request.workbookId,
|
|
1712
|
+
sheetName: request.sheetName,
|
|
1713
|
+
address: request.address
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function summarizeRangeAreas(areas) {
|
|
1717
|
+
const summary = {};
|
|
1718
|
+
assignIfDefined(summary, "address", optionalValue(areas.address));
|
|
1719
|
+
assignIfDefined(summary, "areaCount", optionalValue(areas.areaCount));
|
|
1720
|
+
assignIfDefined(summary, "cellCount", optionalValue(areas.cellCount));
|
|
1721
|
+
assignIfDefined(summary, "isNullObject", optionalValue(areas.isNullObject));
|
|
1722
|
+
return summary;
|
|
1723
|
+
}
|
|
1724
|
+
function getNamedItem(context, request) {
|
|
1725
|
+
return request.sheetName
|
|
1726
|
+
? context.workbook.worksheets.getItem(request.sheetName).names.getItemOrNullObject(request.name)
|
|
1727
|
+
: context.workbook.names.getItemOrNullObject(request.name);
|
|
1728
|
+
}
|
|
1729
|
+
function nameReference(context, request) {
|
|
1730
|
+
if (request.formula !== undefined) {
|
|
1731
|
+
return request.formula;
|
|
1732
|
+
}
|
|
1733
|
+
if (request.reference === undefined) {
|
|
1734
|
+
return "";
|
|
1735
|
+
}
|
|
1736
|
+
if (request.sheetName !== undefined && !request.reference.startsWith("=")) {
|
|
1737
|
+
return context.workbook.worksheets.getItem(request.sheetName).getRange(stripSheetName(request.reference));
|
|
1738
|
+
}
|
|
1739
|
+
return request.reference;
|
|
1740
|
+
}
|
|
1741
|
+
function nameFormula(request) {
|
|
1742
|
+
if (request.formula !== undefined) {
|
|
1743
|
+
return request.formula;
|
|
1744
|
+
}
|
|
1745
|
+
if (request.reference && request.sheetName && !request.reference.startsWith("=")) {
|
|
1746
|
+
return `=${quoteSheetName(request.sheetName)}!${stripSheetName(request.reference)}`;
|
|
1747
|
+
}
|
|
1748
|
+
return request.reference ?? "";
|
|
1749
|
+
}
|
|
1750
|
+
function loadNameWithRange(item) {
|
|
1751
|
+
item.load("name,scope,type,value,formula,comment,visible");
|
|
1752
|
+
const range = item.getRangeOrNullObject();
|
|
1753
|
+
range.load("address,isNullObject");
|
|
1754
|
+
range.worksheet.load("name");
|
|
1755
|
+
return range;
|
|
1756
|
+
}
|
|
1757
|
+
function materializeNameInfo(workbookId, item, fallbackSheetName, range) {
|
|
1758
|
+
const info = {
|
|
1759
|
+
workbookId: workbookId,
|
|
1760
|
+
name: item.name,
|
|
1761
|
+
scope: item.scope === "Worksheet" ? "worksheet" : "workbook"
|
|
1762
|
+
};
|
|
1763
|
+
assignIfDefined(info, "sheetName", optionalValue(range?.isNullObject === false ? range.worksheet.name : fallbackSheetName));
|
|
1764
|
+
assignIfDefined(info, "type", optionalString(item.type));
|
|
1765
|
+
assignIfDefined(info, "value", optionalValue(item.value));
|
|
1766
|
+
assignIfDefined(info, "formula", optionalString(item.formula));
|
|
1767
|
+
assignIfDefined(info, "comment", optionalValue(item.comment));
|
|
1768
|
+
assignIfDefined(info, "visible", optionalValue(item.visible));
|
|
1769
|
+
if (range?.isNullObject === false) {
|
|
1770
|
+
assignIfDefined(info, "address", optionalValue(range.address));
|
|
1771
|
+
}
|
|
1772
|
+
return info;
|
|
1773
|
+
}
|
|
1774
|
+
async function readPivotTableInfo(context, workbookId, pivot) {
|
|
1775
|
+
pivot.load("name,id,refreshOnOpen,useCustomSortLists,enableDataValueEditing,allowMultipleFiltersPerField");
|
|
1776
|
+
pivot.worksheet.load("name");
|
|
1777
|
+
const pivotRange = pivot.layout.getRange();
|
|
1778
|
+
pivotRange.load("address,rowCount,columnCount");
|
|
1779
|
+
pivot.layout.load("altTextDescription,altTextTitle,autoFormat,emptyCellText,enableFieldList,fillEmptyCells,layoutType,preserveFormatting,showColumnGrandTotals,showFieldHeaders,showRowGrandTotals,subtotalLocation");
|
|
1780
|
+
pivot.hierarchies.load("items/name,items/id");
|
|
1781
|
+
pivot.rowHierarchies.load("items/name,items/id,items/position");
|
|
1782
|
+
pivot.columnHierarchies.load("items/name,items/id,items/position");
|
|
1783
|
+
pivot.filterHierarchies.load("items/name,items/id,items/position,items/enableMultipleFilterItems");
|
|
1784
|
+
pivot.dataHierarchies.load("items/name,items/id,items/position,items/numberFormat,items/summarizeBy");
|
|
1785
|
+
const source = pivot.getDataSourceString();
|
|
1786
|
+
const sourceType = pivot.getDataSourceType();
|
|
1787
|
+
await context.sync();
|
|
1788
|
+
const axisHierarchies = [
|
|
1789
|
+
...pivot.rowHierarchies.items,
|
|
1790
|
+
...pivot.columnHierarchies.items,
|
|
1791
|
+
...pivot.filterHierarchies.items
|
|
1792
|
+
];
|
|
1793
|
+
for (const hierarchy of axisHierarchies) {
|
|
1794
|
+
hierarchy.fields.load("items/name,items/id,items/showAllItems,items/subtotals");
|
|
1795
|
+
}
|
|
1796
|
+
for (const hierarchy of pivot.dataHierarchies.items) {
|
|
1797
|
+
hierarchy.field.load("name,id,showAllItems,subtotals");
|
|
1798
|
+
}
|
|
1799
|
+
await context.sync();
|
|
1800
|
+
const info = {
|
|
1801
|
+
workbookId: workbookId,
|
|
1802
|
+
pivotTableName: pivot.name
|
|
1803
|
+
};
|
|
1804
|
+
assignIfDefined(info, "id", optionalValue(pivot.id));
|
|
1805
|
+
assignIfDefined(info, "sheetName", optionalValue(pivot.worksheet.name));
|
|
1806
|
+
assignIfDefined(info, "range", {
|
|
1807
|
+
address: pivotRange.address,
|
|
1808
|
+
rowCount: pivotRange.rowCount,
|
|
1809
|
+
columnCount: pivotRange.columnCount
|
|
1810
|
+
});
|
|
1811
|
+
assignIfDefined(info, "source", optionalValue(source.value));
|
|
1812
|
+
assignIfDefined(info, "sourceType", optionalValue(String(sourceType.value)));
|
|
1813
|
+
assignIfDefined(info, "refreshOnOpen", optionalValue(pivot.refreshOnOpen));
|
|
1814
|
+
assignIfDefined(info, "useCustomSortLists", optionalValue(pivot.useCustomSortLists));
|
|
1815
|
+
assignIfDefined(info, "enableDataValueEditing", optionalValue(pivot.enableDataValueEditing));
|
|
1816
|
+
assignIfDefined(info, "allowMultipleFiltersPerField", optionalValue(pivot.allowMultipleFiltersPerField));
|
|
1817
|
+
const layout = {};
|
|
1818
|
+
assignIfDefined(layout, "altTextDescription", optionalValue(pivot.layout.altTextDescription));
|
|
1819
|
+
assignIfDefined(layout, "altTextTitle", optionalValue(pivot.layout.altTextTitle));
|
|
1820
|
+
assignIfDefined(layout, "autoFormat", optionalValue(pivot.layout.autoFormat));
|
|
1821
|
+
assignIfDefined(layout, "emptyCellText", optionalValue(pivot.layout.emptyCellText));
|
|
1822
|
+
assignIfDefined(layout, "enableFieldList", optionalValue(pivot.layout.enableFieldList));
|
|
1823
|
+
assignIfDefined(layout, "fillEmptyCells", optionalValue(pivot.layout.fillEmptyCells));
|
|
1824
|
+
assignIfDefined(layout, "layoutType", optionalString(pivot.layout.layoutType));
|
|
1825
|
+
assignIfDefined(layout, "preserveFormatting", optionalValue(pivot.layout.preserveFormatting));
|
|
1826
|
+
assignIfDefined(layout, "showColumnGrandTotals", optionalValue(pivot.layout.showColumnGrandTotals));
|
|
1827
|
+
assignIfDefined(layout, "showFieldHeaders", optionalValue(pivot.layout.showFieldHeaders));
|
|
1828
|
+
assignIfDefined(layout, "showRowGrandTotals", optionalValue(pivot.layout.showRowGrandTotals));
|
|
1829
|
+
assignIfDefined(layout, "subtotalLocation", optionalString(pivot.layout.subtotalLocation));
|
|
1830
|
+
assignIfDefined(info, "layout", layout);
|
|
1831
|
+
assignIfDefined(info, "hierarchies", pivot.hierarchies.items.map((hierarchy) => ({
|
|
1832
|
+
...(hierarchy.id !== undefined ? { id: hierarchy.id } : {}),
|
|
1833
|
+
name: hierarchy.name
|
|
1834
|
+
})));
|
|
1835
|
+
assignIfDefined(info, "rowHierarchies", pivot.rowHierarchies.items.map(materializeAxisHierarchyInfo));
|
|
1836
|
+
assignIfDefined(info, "columnHierarchies", pivot.columnHierarchies.items.map(materializeAxisHierarchyInfo));
|
|
1837
|
+
assignIfDefined(info, "filterHierarchies", pivot.filterHierarchies.items.map(materializeAxisHierarchyInfo));
|
|
1838
|
+
assignIfDefined(info, "dataHierarchies", pivot.dataHierarchies.items.map(materializeDataHierarchyInfo));
|
|
1839
|
+
return info;
|
|
1840
|
+
}
|
|
1841
|
+
function materializeAxisHierarchyInfo(hierarchy) {
|
|
1842
|
+
const info = {
|
|
1843
|
+
name: hierarchy.name
|
|
1844
|
+
};
|
|
1845
|
+
assignIfDefined(info, "id", optionalValue(hierarchy.id));
|
|
1846
|
+
assignIfDefined(info, "position", optionalValue(hierarchy.position));
|
|
1847
|
+
if ("enableMultipleFilterItems" in hierarchy) {
|
|
1848
|
+
assignIfDefined(info, "enableMultipleFilterItems", optionalValue(hierarchy.enableMultipleFilterItems));
|
|
1849
|
+
}
|
|
1850
|
+
assignIfDefined(info, "fields", hierarchy.fields.items.map(materializePivotFieldInfo));
|
|
1851
|
+
return info;
|
|
1852
|
+
}
|
|
1853
|
+
function materializeDataHierarchyInfo(hierarchy) {
|
|
1854
|
+
const info = {
|
|
1855
|
+
name: hierarchy.name
|
|
1856
|
+
};
|
|
1857
|
+
assignIfDefined(info, "id", optionalValue(hierarchy.id));
|
|
1858
|
+
assignIfDefined(info, "position", optionalValue(hierarchy.position));
|
|
1859
|
+
assignIfDefined(info, "numberFormat", optionalValue(hierarchy.numberFormat));
|
|
1860
|
+
assignIfDefined(info, "summarizeBy", optionalString(hierarchy.summarizeBy));
|
|
1861
|
+
assignIfDefined(info, "field", materializePivotFieldInfo(hierarchy.field));
|
|
1862
|
+
return info;
|
|
1863
|
+
}
|
|
1864
|
+
function materializePivotFieldInfo(field) {
|
|
1865
|
+
const info = {
|
|
1866
|
+
name: field.name
|
|
1867
|
+
};
|
|
1868
|
+
assignIfDefined(info, "id", optionalValue(field.id));
|
|
1869
|
+
assignIfDefined(info, "showAllItems", optionalValue(field.showAllItems));
|
|
1870
|
+
assignIfDefined(info, "subtotals", optionalValue(field.subtotals));
|
|
1871
|
+
return info;
|
|
1872
|
+
}
|
|
1873
|
+
function loadPivotTemplateReplayObjects(pivot) {
|
|
1874
|
+
pivot.layout.load("altTextDescription,altTextTitle,autoFormat,emptyCellText,enableFieldList,fillEmptyCells,layoutType,preserveFormatting,showColumnGrandTotals,showFieldHeaders,showRowGrandTotals,subtotalLocation");
|
|
1875
|
+
pivot.hierarchies.load("items/name,items/id");
|
|
1876
|
+
pivot.rowHierarchies.load("items/name,items/id,items/position");
|
|
1877
|
+
pivot.columnHierarchies.load("items/name,items/id,items/position");
|
|
1878
|
+
pivot.filterHierarchies.load("items/name,items/id,items/position,items/enableMultipleFilterItems");
|
|
1879
|
+
pivot.dataHierarchies.load("items/name,items/id,items/position");
|
|
1880
|
+
}
|
|
1881
|
+
function hasPivotCreateLayout(request) {
|
|
1882
|
+
return Boolean(request.layout !== undefined ||
|
|
1883
|
+
request.rowFields?.length ||
|
|
1884
|
+
request.columnFields?.length ||
|
|
1885
|
+
request.filterFields?.length ||
|
|
1886
|
+
request.dataFields?.length);
|
|
1887
|
+
}
|
|
1888
|
+
function applyPivotCreateLayout(request, targetPivot) {
|
|
1889
|
+
if (request.layout !== undefined) {
|
|
1890
|
+
targetPivot.layout.set(request.layout);
|
|
1891
|
+
}
|
|
1892
|
+
replayPivotAxis(targetPivot, "row", (request.rowFields ?? []).map(pivotAxisInfoFromName));
|
|
1893
|
+
replayPivotAxis(targetPivot, "column", (request.columnFields ?? []).map(pivotAxisInfoFromName));
|
|
1894
|
+
replayPivotAxis(targetPivot, "filter", (request.filterFields ?? []).map(pivotAxisInfoFromName));
|
|
1895
|
+
replayPivotDataHierarchies(targetPivot, (request.dataFields ?? []).map((field, index) => {
|
|
1896
|
+
const info = {
|
|
1897
|
+
name: field.name ?? field.sourceFieldName,
|
|
1898
|
+
position: index,
|
|
1899
|
+
field: { name: field.sourceFieldName }
|
|
1900
|
+
};
|
|
1901
|
+
assignIfDefined(info, "numberFormat", field.numberFormat);
|
|
1902
|
+
assignIfDefined(info, "summarizeBy", field.summarizeBy);
|
|
1903
|
+
return info;
|
|
1904
|
+
}));
|
|
1905
|
+
}
|
|
1906
|
+
function pivotAxisInfoFromName(name, index) {
|
|
1907
|
+
return {
|
|
1908
|
+
name,
|
|
1909
|
+
position: index
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function applyPivotTemplateMetadata(source, targetPivot, dimensions) {
|
|
1913
|
+
const includes = (dimension) => !dimensions || dimensions.includes(dimension);
|
|
1914
|
+
const copied = [];
|
|
1915
|
+
if (includes("metadata")) {
|
|
1916
|
+
targetPivot.refreshOnOpen = source.refreshOnOpen ?? targetPivot.refreshOnOpen;
|
|
1917
|
+
targetPivot.useCustomSortLists = source.useCustomSortLists ?? targetPivot.useCustomSortLists;
|
|
1918
|
+
targetPivot.enableDataValueEditing = source.enableDataValueEditing ?? targetPivot.enableDataValueEditing;
|
|
1919
|
+
targetPivot.allowMultipleFiltersPerField = source.allowMultipleFiltersPerField ?? targetPivot.allowMultipleFiltersPerField;
|
|
1920
|
+
copied.push("refreshOnOpen", "useCustomSortLists", "enableDataValueEditing", "allowMultipleFiltersPerField");
|
|
1921
|
+
}
|
|
1922
|
+
if (includes("layout") && source.layout !== undefined) {
|
|
1923
|
+
targetPivot.layout.set(source.layout);
|
|
1924
|
+
copied.push("layout");
|
|
1925
|
+
}
|
|
1926
|
+
if (includes("fields")) {
|
|
1927
|
+
replayPivotAxis(targetPivot, "row", source.rowHierarchies ?? []);
|
|
1928
|
+
replayPivotAxis(targetPivot, "column", source.columnHierarchies ?? []);
|
|
1929
|
+
copied.push("rowHierarchyPositions", "columnHierarchyPositions", "fieldSettings");
|
|
1930
|
+
}
|
|
1931
|
+
if (includes("filters")) {
|
|
1932
|
+
replayPivotAxis(targetPivot, "filter", source.filterHierarchies ?? []);
|
|
1933
|
+
copied.push("filterHierarchyPositions");
|
|
1934
|
+
}
|
|
1935
|
+
if (includes("dataFields")) {
|
|
1936
|
+
replayPivotDataHierarchies(targetPivot, source.dataHierarchies ?? [], includes("numberFormats"));
|
|
1937
|
+
copied.push("dataHierarchySettings");
|
|
1938
|
+
if (includes("numberFormats")) {
|
|
1939
|
+
copied.push("dataHierarchyNumberFormats");
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (includes("refresh")) {
|
|
1943
|
+
copied.push("refresh");
|
|
1944
|
+
}
|
|
1945
|
+
return copied;
|
|
1946
|
+
}
|
|
1947
|
+
function replayPivotAxis(targetPivot, axis, hierarchies) {
|
|
1948
|
+
if (axis === "filter") {
|
|
1949
|
+
replayFilterPivotAxis(targetPivot, hierarchies);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const collection = axis === "row" ? targetPivot.rowHierarchies : targetPivot.columnHierarchies;
|
|
1953
|
+
for (const existing of collection.items) {
|
|
1954
|
+
collection.remove(existing);
|
|
1955
|
+
}
|
|
1956
|
+
for (const [index, hierarchyInfo] of hierarchies.entries()) {
|
|
1957
|
+
const hierarchy = targetPivot.hierarchies.getItem(hierarchyInfo.name);
|
|
1958
|
+
const added = collection.add(hierarchy);
|
|
1959
|
+
added.position = hierarchyInfo.position ?? index;
|
|
1960
|
+
for (const fieldInfo of hierarchyInfo.fields ?? []) {
|
|
1961
|
+
const field = added.fields.getItem(fieldInfo.name);
|
|
1962
|
+
applyPivotFieldMetadata(field, fieldInfo);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function replayFilterPivotAxis(targetPivot, hierarchies) {
|
|
1967
|
+
for (const existing of targetPivot.filterHierarchies.items) {
|
|
1968
|
+
targetPivot.filterHierarchies.remove(existing);
|
|
1969
|
+
}
|
|
1970
|
+
for (const [index, hierarchyInfo] of hierarchies.entries()) {
|
|
1971
|
+
const hierarchy = targetPivot.hierarchies.getItem(hierarchyInfo.name);
|
|
1972
|
+
const added = targetPivot.filterHierarchies.add(hierarchy);
|
|
1973
|
+
added.position = hierarchyInfo.position ?? index;
|
|
1974
|
+
if (hierarchyInfo.enableMultipleFilterItems !== undefined) {
|
|
1975
|
+
added.enableMultipleFilterItems = hierarchyInfo.enableMultipleFilterItems;
|
|
1976
|
+
}
|
|
1977
|
+
for (const fieldInfo of hierarchyInfo.fields ?? []) {
|
|
1978
|
+
const field = added.fields.getItem(fieldInfo.name);
|
|
1979
|
+
applyPivotFieldMetadata(field, fieldInfo);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function replayPivotDataHierarchies(targetPivot, hierarchies, includeNumberFormats = true) {
|
|
1984
|
+
for (const existing of targetPivot.dataHierarchies.items) {
|
|
1985
|
+
targetPivot.dataHierarchies.remove(existing);
|
|
1986
|
+
}
|
|
1987
|
+
for (const [index, hierarchyInfo] of hierarchies.entries()) {
|
|
1988
|
+
const sourceFieldName = hierarchyInfo.field?.name ?? hierarchyInfo.name;
|
|
1989
|
+
const added = targetPivot.dataHierarchies.add(targetPivot.hierarchies.getItem(sourceFieldName));
|
|
1990
|
+
added.position = hierarchyInfo.position ?? index;
|
|
1991
|
+
if (hierarchyInfo.name !== undefined) {
|
|
1992
|
+
added.name = hierarchyInfo.name;
|
|
1993
|
+
}
|
|
1994
|
+
if (includeNumberFormats && hierarchyInfo.numberFormat !== undefined) {
|
|
1995
|
+
added.numberFormat = hierarchyInfo.numberFormat;
|
|
1996
|
+
}
|
|
1997
|
+
if (hierarchyInfo.summarizeBy !== undefined) {
|
|
1998
|
+
added.summarizeBy = hierarchyInfo.summarizeBy;
|
|
1999
|
+
}
|
|
2000
|
+
if (hierarchyInfo.field !== undefined) {
|
|
2001
|
+
applyPivotFieldMetadata(added.field, hierarchyInfo.field);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function applyPivotFieldMetadata(field, fieldInfo) {
|
|
2006
|
+
if (fieldInfo.showAllItems !== undefined) {
|
|
2007
|
+
field.showAllItems = fieldInfo.showAllItems;
|
|
2008
|
+
}
|
|
2009
|
+
if (fieldInfo.subtotals !== undefined) {
|
|
2010
|
+
field.subtotals = fieldInfo.subtotals;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function loadChartInfoObjects(workbookId, sheetName, chart) {
|
|
2014
|
+
chart.load("name,id,chartType,top,left,width,height,style,plotBy");
|
|
2015
|
+
chart.title.load("text");
|
|
2016
|
+
return { workbookId, sheetName, chart };
|
|
2017
|
+
}
|
|
2018
|
+
function materializeChartInfo(loaded) {
|
|
2019
|
+
const info = {
|
|
2020
|
+
workbookId: loaded.workbookId,
|
|
2021
|
+
sheetName: loaded.sheetName,
|
|
2022
|
+
chartName: loaded.chart.name
|
|
2023
|
+
};
|
|
2024
|
+
assignIfDefined(info, "id", optionalValue(loaded.chart.id));
|
|
2025
|
+
assignIfDefined(info, "chartType", optionalValue(String(loaded.chart.chartType)));
|
|
2026
|
+
assignIfDefined(info, "top", optionalValue(loaded.chart.top));
|
|
2027
|
+
assignIfDefined(info, "left", optionalValue(loaded.chart.left));
|
|
2028
|
+
assignIfDefined(info, "width", optionalValue(loaded.chart.width));
|
|
2029
|
+
assignIfDefined(info, "height", optionalValue(loaded.chart.height));
|
|
2030
|
+
assignIfDefined(info, "style", optionalValue(loaded.chart.style));
|
|
2031
|
+
assignIfDefined(info, "plotBy", optionalValue(String(loaded.chart.plotBy)));
|
|
2032
|
+
assignIfDefined(info, "title", optionalValue(loaded.chart.title.text));
|
|
2033
|
+
return info;
|
|
2034
|
+
}
|
|
2035
|
+
function loadTableInfoObjects(table) {
|
|
2036
|
+
table.load("name,id,style,showHeaders,showTotals,showFilterButton,showBandedRows,showBandedColumns");
|
|
2037
|
+
const range = table.getRange();
|
|
2038
|
+
const headerRange = table.getHeaderRowRange();
|
|
2039
|
+
const columns = table.columns;
|
|
2040
|
+
const autoFilter = table.autoFilter;
|
|
2041
|
+
const sort = table.sort;
|
|
2042
|
+
range.load("address,rowCount,columnCount");
|
|
2043
|
+
range.worksheet.load("name");
|
|
2044
|
+
headerRange.load("address");
|
|
2045
|
+
columns.load("count,items/id,items/index,items/name");
|
|
2046
|
+
table.rows.load("count");
|
|
2047
|
+
autoFilter.load("criteria,enabled,isDataFiltered");
|
|
2048
|
+
sort.load("fields,matchCase,method");
|
|
2049
|
+
return { table, range, headerRange, columns, autoFilter, sort };
|
|
2050
|
+
}
|
|
2051
|
+
function materializeTableInfo(workbookId, loaded) {
|
|
2052
|
+
const info = {
|
|
2053
|
+
workbookId: workbookId,
|
|
2054
|
+
tableName: loaded.table.name,
|
|
2055
|
+
rowCount: loaded.table.rows.count,
|
|
2056
|
+
columnCount: loaded.columns.count,
|
|
2057
|
+
columns: loaded.columns.items.map((column) => ({
|
|
2058
|
+
id: column.id,
|
|
2059
|
+
index: column.index,
|
|
2060
|
+
name: column.name
|
|
2061
|
+
}))
|
|
2062
|
+
};
|
|
2063
|
+
assignIfDefined(info, "id", optionalValue(loaded.table.id));
|
|
2064
|
+
assignIfDefined(info, "sheetName", optionalValue(loaded.range.worksheet.name));
|
|
2065
|
+
assignIfDefined(info, "address", optionalValue(loaded.range.address));
|
|
2066
|
+
assignIfDefined(info, "headerAddress", optionalValue(loaded.headerRange.address));
|
|
2067
|
+
assignIfDefined(info, "style", optionalValue(loaded.table.style));
|
|
2068
|
+
assignIfDefined(info, "showHeaders", optionalValue(loaded.table.showHeaders));
|
|
2069
|
+
assignIfDefined(info, "showTotals", optionalValue(loaded.table.showTotals));
|
|
2070
|
+
assignIfDefined(info, "showFilterButton", optionalValue(loaded.table.showFilterButton));
|
|
2071
|
+
assignIfDefined(info, "showBandedRows", optionalValue(loaded.table.showBandedRows));
|
|
2072
|
+
assignIfDefined(info, "showBandedColumns", optionalValue(loaded.table.showBandedColumns));
|
|
2073
|
+
assignIfDefined(info, "filters", {
|
|
2074
|
+
enabled: loaded.autoFilter.enabled,
|
|
2075
|
+
isDataFiltered: loaded.autoFilter.isDataFiltered,
|
|
2076
|
+
criteria: loaded.autoFilter.criteria
|
|
2077
|
+
});
|
|
2078
|
+
assignIfDefined(info, "sort", {
|
|
2079
|
+
fields: loaded.sort.fields,
|
|
2080
|
+
matchCase: loaded.sort.matchCase,
|
|
2081
|
+
method: loaded.sort.method
|
|
2082
|
+
});
|
|
2083
|
+
return info;
|
|
2084
|
+
}
|
|
2085
|
+
async function mutateTableAndReturnInfo(request, mutate) {
|
|
2086
|
+
return Excel.run(async (context) => {
|
|
2087
|
+
const table = context.workbook.tables.getItem(request.tableName);
|
|
2088
|
+
mutate(table, context);
|
|
2089
|
+
const loaded = loadTableInfoObjects(table);
|
|
2090
|
+
await context.sync();
|
|
2091
|
+
return { ok: true, info: materializeTableInfo(request.workbookId, loaded) };
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
function getRange(context, target) {
|
|
2095
|
+
return context.workbook.worksheets.getItem(target.sheetName).getRange(stripSheetName(target.address));
|
|
2096
|
+
}
|
|
2097
|
+
function loadSnapshotProperties(range) {
|
|
2098
|
+
range.load("values, formulas, numberFormat, text, rowCount, columnCount");
|
|
2099
|
+
range.format.load("horizontalAlignment, verticalAlignment, rowHeight, columnWidth");
|
|
2100
|
+
range.format.fill.load("color");
|
|
2101
|
+
range.format.font.load("name, size, color, bold, italic");
|
|
2102
|
+
}
|
|
2103
|
+
function materializeSnapshot(target, range) {
|
|
2104
|
+
const values = range.values;
|
|
2105
|
+
const formulas = range.formulas;
|
|
2106
|
+
const numberFormat = range.numberFormat;
|
|
2107
|
+
const text = range.text;
|
|
2108
|
+
const style = {};
|
|
2109
|
+
assignIfDefined(style, "fillColor", optionalValue(range.format.fill.color));
|
|
2110
|
+
assignIfDefined(style, "fontName", optionalValue(range.format.font.name));
|
|
2111
|
+
assignIfDefined(style, "fontSize", optionalValue(range.format.font.size));
|
|
2112
|
+
assignIfDefined(style, "fontColor", optionalValue(range.format.font.color));
|
|
2113
|
+
assignIfDefined(style, "fontBold", optionalValue(range.format.font.bold));
|
|
2114
|
+
assignIfDefined(style, "fontItalic", optionalValue(range.format.font.italic));
|
|
2115
|
+
assignIfDefined(style, "horizontalAlignment", optionalValue(String(range.format.horizontalAlignment)));
|
|
2116
|
+
assignIfDefined(style, "verticalAlignment", optionalValue(String(range.format.verticalAlignment)));
|
|
2117
|
+
assignIfDefined(style, "rowHeight", optionalValue(range.format.rowHeight));
|
|
2118
|
+
assignIfDefined(style, "columnWidth", optionalValue(range.format.columnWidth));
|
|
2119
|
+
const snapshot = {
|
|
2120
|
+
fingerprint: createRangeFingerprint(target, {
|
|
2121
|
+
values,
|
|
2122
|
+
formulas,
|
|
2123
|
+
numberFormat,
|
|
2124
|
+
text,
|
|
2125
|
+
style
|
|
2126
|
+
}),
|
|
2127
|
+
numberFormat,
|
|
2128
|
+
text,
|
|
2129
|
+
style
|
|
2130
|
+
};
|
|
2131
|
+
if (values !== undefined) {
|
|
2132
|
+
snapshot.values = values;
|
|
2133
|
+
}
|
|
2134
|
+
if (formulas !== undefined) {
|
|
2135
|
+
snapshot.formulas = formulas;
|
|
2136
|
+
}
|
|
2137
|
+
return snapshot;
|
|
2138
|
+
}
|
|
2139
|
+
function applyTemplateSheetOperation(context, operation, templateSources) {
|
|
2140
|
+
const source = templateSources.find((templateSource) => templateSource.templateId === operation.templateId);
|
|
2141
|
+
if (!source) {
|
|
2142
|
+
return {
|
|
2143
|
+
code: "TEMPLATE_SOURCE_MISSING",
|
|
2144
|
+
message: `Template source not registered for ${operation.templateId}.`,
|
|
2145
|
+
details: { templateId: operation.templateId }
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
const sourceSheet = context.workbook.worksheets.getItem(source.sourceSheetName);
|
|
2149
|
+
const copiedSheet = sourceSheet.copy(Excel.WorksheetPositionType.after, sourceSheet);
|
|
2150
|
+
copiedSheet.name = operation.newSheetName;
|
|
2151
|
+
if (operation.clearDataRegions) {
|
|
2152
|
+
for (const dataRegion of source.dataRegions) {
|
|
2153
|
+
copiedSheet.getRange(stripSheetName(dataRegion)).clear(Excel.ClearApplyTo.contents);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
copiedSheet.activate();
|
|
2157
|
+
return undefined;
|
|
2158
|
+
}
|
|
2159
|
+
function restoreRangeSnapshot(context, operation) {
|
|
2160
|
+
const range = getRange(context, operation.target);
|
|
2161
|
+
if (operation.snapshot.formulas) {
|
|
2162
|
+
range.formulas = operation.snapshot.formulas;
|
|
2163
|
+
}
|
|
2164
|
+
else if (operation.snapshot.values) {
|
|
2165
|
+
range.values = operation.snapshot.values;
|
|
2166
|
+
}
|
|
2167
|
+
if (operation.snapshot.numberFormat) {
|
|
2168
|
+
range.numberFormat = operation.snapshot.numberFormat;
|
|
2169
|
+
}
|
|
2170
|
+
const style = operation.snapshot.style;
|
|
2171
|
+
if (!style) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (style.fillColor) {
|
|
2175
|
+
range.format.fill.color = style.fillColor;
|
|
2176
|
+
}
|
|
2177
|
+
if (style.fontName) {
|
|
2178
|
+
range.format.font.name = style.fontName;
|
|
2179
|
+
}
|
|
2180
|
+
if (style.fontSize !== undefined) {
|
|
2181
|
+
range.format.font.size = style.fontSize;
|
|
2182
|
+
}
|
|
2183
|
+
if (style.fontColor) {
|
|
2184
|
+
range.format.font.color = style.fontColor;
|
|
2185
|
+
}
|
|
2186
|
+
if (style.fontBold !== undefined) {
|
|
2187
|
+
range.format.font.bold = style.fontBold;
|
|
2188
|
+
}
|
|
2189
|
+
if (style.fontItalic !== undefined) {
|
|
2190
|
+
range.format.font.italic = style.fontItalic;
|
|
2191
|
+
}
|
|
2192
|
+
if (style.horizontalAlignment) {
|
|
2193
|
+
range.format.horizontalAlignment = style.horizontalAlignment;
|
|
2194
|
+
}
|
|
2195
|
+
if (style.verticalAlignment) {
|
|
2196
|
+
range.format.verticalAlignment = style.verticalAlignment;
|
|
2197
|
+
}
|
|
2198
|
+
if (style.rowHeight !== undefined) {
|
|
2199
|
+
range.format.rowHeight = style.rowHeight;
|
|
2200
|
+
}
|
|
2201
|
+
if (style.columnWidth !== undefined) {
|
|
2202
|
+
range.format.columnWidth = style.columnWidth;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
function applyRangeStyle(range, style) {
|
|
2206
|
+
if (style.fillColor) {
|
|
2207
|
+
range.format.fill.color = style.fillColor;
|
|
2208
|
+
}
|
|
2209
|
+
if (style.fontName) {
|
|
2210
|
+
range.format.font.name = style.fontName;
|
|
2211
|
+
}
|
|
2212
|
+
if (style.fontSize !== undefined) {
|
|
2213
|
+
range.format.font.size = style.fontSize;
|
|
2214
|
+
}
|
|
2215
|
+
if (style.fontColor) {
|
|
2216
|
+
range.format.font.color = style.fontColor;
|
|
2217
|
+
}
|
|
2218
|
+
if (style.fontBold !== undefined) {
|
|
2219
|
+
range.format.font.bold = style.fontBold;
|
|
2220
|
+
}
|
|
2221
|
+
if (style.fontItalic !== undefined) {
|
|
2222
|
+
range.format.font.italic = style.fontItalic;
|
|
2223
|
+
}
|
|
2224
|
+
if (style.horizontalAlignment) {
|
|
2225
|
+
range.format.horizontalAlignment = style.horizontalAlignment;
|
|
2226
|
+
}
|
|
2227
|
+
if (style.verticalAlignment) {
|
|
2228
|
+
range.format.verticalAlignment = style.verticalAlignment;
|
|
2229
|
+
}
|
|
2230
|
+
if (style.rowHeight !== undefined) {
|
|
2231
|
+
range.format.rowHeight = style.rowHeight;
|
|
2232
|
+
}
|
|
2233
|
+
if (style.columnWidth !== undefined) {
|
|
2234
|
+
range.format.columnWidth = style.columnWidth;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function normalizeFormulaMatrix(matrix) {
|
|
2238
|
+
return matrix.map((row) => row.map((value) => {
|
|
2239
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
return value;
|
|
2243
|
+
}));
|
|
2244
|
+
}
|
|
2245
|
+
function matrixHasFormula(matrix) {
|
|
2246
|
+
for (const row of matrix) {
|
|
2247
|
+
for (const formula of row) {
|
|
2248
|
+
if (formula?.startsWith("=")) {
|
|
2249
|
+
return true;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
function countFormulas(matrix) {
|
|
2256
|
+
let count = 0;
|
|
2257
|
+
for (const row of matrix) {
|
|
2258
|
+
for (const formula of row) {
|
|
2259
|
+
if (formula?.startsWith("=")) {
|
|
2260
|
+
count += 1;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return count;
|
|
2265
|
+
}
|
|
2266
|
+
function toClearApplyTo(applyTo) {
|
|
2267
|
+
switch (applyTo) {
|
|
2268
|
+
case "contents":
|
|
2269
|
+
return Excel.ClearApplyTo.contents;
|
|
2270
|
+
case "formats":
|
|
2271
|
+
return Excel.ClearApplyTo.formats;
|
|
2272
|
+
case "hyperlinks":
|
|
2273
|
+
return Excel.ClearApplyTo.hyperlinks;
|
|
2274
|
+
case "all":
|
|
2275
|
+
return Excel.ClearApplyTo.all;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
function toRangeCopyType(copyType) {
|
|
2279
|
+
switch (copyType) {
|
|
2280
|
+
case "values":
|
|
2281
|
+
return Excel.RangeCopyType.values;
|
|
2282
|
+
case "formats":
|
|
2283
|
+
return Excel.RangeCopyType.formats;
|
|
2284
|
+
case "formulas":
|
|
2285
|
+
return Excel.RangeCopyType.formulas;
|
|
2286
|
+
case "all":
|
|
2287
|
+
return Excel.RangeCopyType.all;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
function toWorksheetPositionType(position) {
|
|
2291
|
+
switch (position) {
|
|
2292
|
+
case "beginning":
|
|
2293
|
+
return Excel.WorksheetPositionType.beginning;
|
|
2294
|
+
case "end":
|
|
2295
|
+
return Excel.WorksheetPositionType.end;
|
|
2296
|
+
case "before":
|
|
2297
|
+
return Excel.WorksheetPositionType.before;
|
|
2298
|
+
case "after":
|
|
2299
|
+
return Excel.WorksheetPositionType.after;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function maybeSuspendExcel(context, estimatedCellsTouched) {
|
|
2303
|
+
if (estimatedCellsTouched >= 10_000) {
|
|
2304
|
+
context.workbook.application.suspendApiCalculationUntilNextSync();
|
|
2305
|
+
context.workbook.application.suspendScreenUpdatingUntilNextSync();
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
function assertMatrixShape(target, matrix) {
|
|
2309
|
+
if (matrix.length === 0 || matrix.some((row) => row.length !== matrix[0].length)) {
|
|
2310
|
+
throw new Error(`Invalid matrix shape for ${target.sheetName}!${target.address}`);
|
|
2311
|
+
}
|
|
2312
|
+
const parsed = parseA1Address(stripSheetName(target.address));
|
|
2313
|
+
if (parsed.endRow - parsed.startRow + 1 !== matrix.length || parsed.endColumn - parsed.startColumn + 1 !== matrix[0].length) {
|
|
2314
|
+
throw new Error(`Matrix dimensions do not match ${target.sheetName}!${target.address}`);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function matrixCellCount(matrix) {
|
|
2318
|
+
return matrix.reduce((count, row) => count + row.length, 0);
|
|
2319
|
+
}
|
|
2320
|
+
function writeMatrixInChunks(context, target, matrix, property) {
|
|
2321
|
+
const parsed = parseA1Address(stripSheetName(target.address));
|
|
2322
|
+
const columnCount = matrix[0]?.length ?? 0;
|
|
2323
|
+
const worksheet = context.workbook.worksheets.getItem(target.sheetName);
|
|
2324
|
+
let chunkCount = 0;
|
|
2325
|
+
for (const chunk of chunkMatrixRows(matrix, CHUNK_CELL_LIMIT)) {
|
|
2326
|
+
const range = worksheet.getRangeByIndexes(parsed.startRow - 1 + chunk.rowOffset, parsed.startColumn - 1, chunk.rows.length, columnCount);
|
|
2327
|
+
range[property] = chunk.rows;
|
|
2328
|
+
chunkCount += 1;
|
|
2329
|
+
}
|
|
2330
|
+
return chunkCount;
|
|
2331
|
+
}
|
|
2332
|
+
function optionalValue(value) {
|
|
2333
|
+
return value === null || value === undefined ? undefined : value;
|
|
2334
|
+
}
|
|
2335
|
+
function getDocumentFile(sliceSize) {
|
|
2336
|
+
return new Promise((resolve, reject) => {
|
|
2337
|
+
Office.context.document.getFileAsync(Office.FileType.Compressed, { sliceSize }, (result) => {
|
|
2338
|
+
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
|
2339
|
+
resolve(result.value);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
reject(new Error(result.error?.message ?? "Office Document.getFileAsync failed."));
|
|
2343
|
+
});
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
function getDocumentFileSlice(file, index) {
|
|
2347
|
+
return new Promise((resolve, reject) => {
|
|
2348
|
+
file.getSliceAsync(index, (result) => {
|
|
2349
|
+
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
|
2350
|
+
resolve(result.value);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
reject(new Error(result.error?.message ?? `Office File.getSliceAsync failed for slice ${index}.`));
|
|
2354
|
+
});
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
function closeDocumentFile(file) {
|
|
2358
|
+
return new Promise((resolve) => {
|
|
2359
|
+
file.closeAsync(() => resolve());
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
function sliceDataToBase64(data) {
|
|
2363
|
+
if (typeof data === "string") {
|
|
2364
|
+
return data;
|
|
2365
|
+
}
|
|
2366
|
+
if (data instanceof Uint8Array) {
|
|
2367
|
+
return uint8ArrayToBase64(data);
|
|
2368
|
+
}
|
|
2369
|
+
if (Array.isArray(data)) {
|
|
2370
|
+
return uint8ArrayToBase64(Uint8Array.from(data));
|
|
2371
|
+
}
|
|
2372
|
+
if (data instanceof ArrayBuffer) {
|
|
2373
|
+
return uint8ArrayToBase64(new Uint8Array(data));
|
|
2374
|
+
}
|
|
2375
|
+
throw new Error("Unsupported Office file slice data format.");
|
|
2376
|
+
}
|
|
2377
|
+
function uint8ArrayToBase64(bytes) {
|
|
2378
|
+
let binary = "";
|
|
2379
|
+
const chunkSize = 0x8000;
|
|
2380
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
2381
|
+
const chunk = bytes.subarray(offset, offset + chunkSize);
|
|
2382
|
+
binary += String.fromCharCode(...chunk);
|
|
2383
|
+
}
|
|
2384
|
+
return btoa(binary);
|
|
2385
|
+
}
|
|
2386
|
+
function getCustomXmlParts(context) {
|
|
2387
|
+
const workbook = context.workbook;
|
|
2388
|
+
return workbook.customXmlParts;
|
|
2389
|
+
}
|
|
2390
|
+
function workbookLocalConfigXml(config) {
|
|
2391
|
+
const json = JSON.stringify(config);
|
|
2392
|
+
return [
|
|
2393
|
+
`<owb:localConfig xmlns:owb="${OPEN_WORKBOOK_CUSTOM_XML_NAMESPACE}" version="1" workbookId="${escapeXmlAttribute(config.workbookId)}">`,
|
|
2394
|
+
`<owb:json>${cdata(json)}</owb:json>`,
|
|
2395
|
+
"</owb:localConfig>"
|
|
2396
|
+
].join("");
|
|
2397
|
+
}
|
|
2398
|
+
function parseWorkbookLocalConfigXml(xml) {
|
|
2399
|
+
const cdataMatch = xml.match(/<owb:json>\s*<!\[CDATA\[([\s\S]*)\]\]>\s*<\/owb:json>/);
|
|
2400
|
+
const textMatch = xml.match(/<owb:json>([\s\S]*?)<\/owb:json>/);
|
|
2401
|
+
const rawJson = cdataMatch?.[1] ?? (textMatch?.[1] ? unescapeXmlText(textMatch[1]) : undefined);
|
|
2402
|
+
if (!rawJson) {
|
|
2403
|
+
throw new Error("Open Workbook custom XML part does not contain local config JSON.");
|
|
2404
|
+
}
|
|
2405
|
+
return JSON.parse(rawJson);
|
|
2406
|
+
}
|
|
2407
|
+
function cdata(value) {
|
|
2408
|
+
return `<![CDATA[${value.replaceAll("]]>", "]]]]><![CDATA[>")}]]>`;
|
|
2409
|
+
}
|
|
2410
|
+
function escapeXmlAttribute(value) {
|
|
2411
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
2412
|
+
}
|
|
2413
|
+
function unescapeXmlText(value) {
|
|
2414
|
+
return value.replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("&", "&");
|
|
2415
|
+
}
|
|
2416
|
+
function assignIfDefined(target, key, value) {
|
|
2417
|
+
if (value !== undefined) {
|
|
2418
|
+
target[key] = value;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function stripSheetName(address) {
|
|
2422
|
+
const bangIndex = address.lastIndexOf("!");
|
|
2423
|
+
return bangIndex >= 0 ? address.slice(bangIndex + 1) : address;
|
|
2424
|
+
}
|
|
2425
|
+
function cellPositionFromZeroBased(workbookId, sheetName, rowIndex, columnIndex) {
|
|
2426
|
+
const row = rowIndex + 1;
|
|
2427
|
+
const column = columnIndex + 1;
|
|
2428
|
+
return {
|
|
2429
|
+
workbookId,
|
|
2430
|
+
sheetName,
|
|
2431
|
+
address: formatA1Cell(row, column),
|
|
2432
|
+
row,
|
|
2433
|
+
column,
|
|
2434
|
+
rowIndex,
|
|
2435
|
+
columnIndex
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
function quoteSheetName(sheetName) {
|
|
2439
|
+
return `'${sheetName.replace(/'/g, "''")}'`;
|
|
2440
|
+
}
|
|
2441
|
+
function optionalString(value) {
|
|
2442
|
+
return value === null || value === undefined ? undefined : String(value);
|
|
2443
|
+
}
|
|
2444
|
+
function createTelemetry(started, counters, warnings) {
|
|
2445
|
+
return {
|
|
2446
|
+
durationMs: Math.round(performance.now() - started),
|
|
2447
|
+
syncCount: counters.syncCount,
|
|
2448
|
+
payloadBytes: 0,
|
|
2449
|
+
cellsRead: counters.cellsRead,
|
|
2450
|
+
cellsWritten: counters.cellsWritten,
|
|
2451
|
+
rangeCount: counters.rangeCount,
|
|
2452
|
+
chunkCount: Math.max(1, counters.chunkCount),
|
|
2453
|
+
engineName: ENGINE_NAME,
|
|
2454
|
+
engineVersion: ENGINE_VERSION,
|
|
2455
|
+
warningCount: warnings.length
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function detectPlatform() {
|
|
2459
|
+
if (Office.context.platform === Office.PlatformType.Mac) {
|
|
2460
|
+
return "mac";
|
|
2461
|
+
}
|
|
2462
|
+
if (Office.context.platform === Office.PlatformType.PC) {
|
|
2463
|
+
return "windows";
|
|
2464
|
+
}
|
|
2465
|
+
if (Office.context.platform === Office.PlatformType.OfficeOnline) {
|
|
2466
|
+
return "web";
|
|
2467
|
+
}
|
|
2468
|
+
return "unknown";
|
|
2469
|
+
}
|
|
2470
|
+
function isOfficeApiSetSupported(set, version) {
|
|
2471
|
+
try {
|
|
2472
|
+
return Office.context.requirements.isSetSupported(set, version);
|
|
2473
|
+
}
|
|
2474
|
+
catch {
|
|
2475
|
+
return false;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function hostCapability(name, supported, set, version) {
|
|
2479
|
+
return {
|
|
2480
|
+
name,
|
|
2481
|
+
supported,
|
|
2482
|
+
status: supported ? "supported" : "unsupported",
|
|
2483
|
+
...(supported ? {} : { reason: `${set} ${version} is not supported by this Excel host.` }),
|
|
2484
|
+
requires: [{ set, version }]
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
//# sourceMappingURL=excel-executor.js.map
|