Skip to content

Commit 1870121

Browse files
authored
Merge pull request #117 from myk93/makeTemplateGeneral
Allow user-defined table and sheet names, flexible table positioning
2 parents 7c064b4 + af07b85 commit 1870121

20 files changed

+371
-78
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@microsoft/connected-workbooks",
3-
"version": "3.2.1-beta",
3+
"version": "3.3.0-beta",
44
"description": "Microsoft backed, Excel advanced xlsx workbook generation JavaScript library",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -37,11 +37,11 @@
3737
},
3838
"homepage": "https://github.com/microsoft/connected-workbooks#readme",
3939
"dependencies": {
40+
"@xmldom/xmldom": "~0.8.4",
4041
"base64-js": "^1.5.1",
4142
"buffer": "^6.0.3",
4243
"jszip": "^3.5.0",
4344
"uuid": "^9.0.0",
44-
"@xmldom/xmldom": "~0.8.4",
4545
"xmldom-qsa": "^1.1.3"
4646
},
4747
"devDependencies": {

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export interface FileConfigs {
4141
templateFile?: File;
4242
docProps?: DocProps;
4343
hostName?: string;
44+
templateSettings?: TemplateSettings;
45+
}
46+
47+
export interface TemplateSettings {
48+
tableName?: string;
49+
sheetName?: string;
4450
}
4551

4652
export enum DataTypes {

src/utils/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ export const tableXmlPath = "xl/tables/table1.xml";
88
export const queryTableXmlPath = "xl/queryTables/queryTable1.xml";
99
export const workbookXmlPath = "xl/workbook.xml";
1010
export const queryTablesPath = "xl/queryTables/";
11+
export const tablesFolderPath = "xl/tables/";
1112
export const pivotCachesPath = "xl/pivotCache/";
1213
export const section1mPath = "Formulas/Section1.m";
1314
export const docPropsCoreXmlPath = "docProps/core.xml";
1415
export const relsXmlPath = "_rels/.rels";
1516
export const docMetadataXmlPath = "docMetadata";
1617
export const docPropsRootElement = "cp:coreProperties";
18+
export const workbookRelsXmlPath = "xl/_rels/workbook.xml.rels";
1719

1820
export const sharedStringsNotFoundErr = "SharedStrings were not found in template";
1921
export const connectionsNotFoundErr = "Connections were not found in template";
22+
export const WorkbookNotFoundERR = "workbook was not found in template";
2023
export const sheetsNotFoundErr = "Sheets were not found in template";
2124
export const base64NotFoundErr = "Base64 was not found in template";
2225
export const emptyQueryMashupErr = "Query mashup is empty";
@@ -27,6 +30,7 @@ export const formulaSectionNotFoundErr = "Formula section wasn't found in templa
2730
export const templateWithInitialDataErr = "Cannot use a template file with initial data";
2831
export const queryTableNotFoundErr = "Query table wasn't found in template";
2932
export const tableNotFoundErr = "Table wasn't found in template";
33+
export const tableReferenceNotFoundErr = "Reference not found in the table XML.";
3034
export const invalidValueInColumnErr = "Invalid cell value in column";
3135
export const headerNotFoundErr = "Invalid JSON file, header is missing";
3236
export const invalidDataTypeErr = "Invalid JSON file, invalid data type";
@@ -39,6 +43,7 @@ export const promotedHeadersCannotBeUsedWithoutAdjustingColumnNamesErr = "Header
3943
export const unexpectedErr = "Unexpected error";
4044
export const arrayIsntMxNErr = "Array isn't MxN";
4145
export const relsNotFoundErr = ".rels were not found in template";
46+
export const xlRelsNotFoundErr = "workbook.xml.rels were not found xl";
4247
export const columnIndexOutOfRangeErr = "Column index out of range";
4348

4449
export const blobFileType = "blob";
@@ -85,6 +90,7 @@ export const element = {
8590
dimension: "dimension",
8691
selection: "selection",
8792
kindCell: "c",
93+
sheet: "sheet",
8894
};
8995

9096
export const elementAttributes = {
@@ -99,6 +105,7 @@ export const elementAttributes = {
99105
name: "name",
100106
description: "description",
101107
id: "id",
108+
relationId: "r:id",
102109
type: "Type",
103110
value: "Value",
104111
relationshipInfo: "RelationshipInfoContainer",
@@ -118,6 +125,7 @@ export const elementAttributes = {
118125
x14acDyDescent: "x14ac:dyDescent",
119126
xr3uid: "xr3:uid",
120127
space: "xml:space",
128+
target: "Target",
121129
};
122130

123131
export const dataTypeKind = {
@@ -138,6 +146,7 @@ export const defaults = {
138146
queryName: "Query1",
139147
sheetName: "Sheet1",
140148
columnName: "Column",
149+
tableName: "Table1",
141150
};
142151

143152
export const URLS = {

src/utils/documentUtils.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,32 @@ const convertToExcelColumn = (index: number): string => {
6969
const base = 26; // number of letters in the alphabet
7070
while (index >= 0) {
7171
const remainder = index % base;
72-
columnStr = String.fromCharCode(remainder + 65) + columnStr; // ASCII 'A' is 65
72+
columnStr = String.fromCharCode(remainder + 'A'.charCodeAt(0)) + columnStr;
7373
index = Math.floor(index / base) - 1;
7474
}
7575

7676
return columnStr;
7777
};
7878

79-
const getTableReference = (numberOfCols: number, numberOfRows: number): string => {
80-
return `A1:${getCellReferenceRelative(numberOfCols, numberOfRows)}`;
81-
};
79+
/**
80+
* Parse an Excel range (e.g. "B2:D10") and return its starting row and column indices.
81+
* @param cellRangeRef - Range reference string.
82+
* @returns Object with numeric row and column.
83+
*/
84+
const GetStartPosition = (cellRangeRef: string): { row: number; column: number } => {
85+
const match = cellRangeRef.toUpperCase().match(/^([A-Z]+)(\d+):/);
86+
if (!match) {
87+
return { row: 0, column: 0 };
88+
}
89+
90+
const [, colLetters, rowStr] = match;
91+
const row = parseInt(rowStr, 10);
92+
const column = colLetters
93+
.split("")
94+
.reduce((acc, char) => acc * 26 + (char.charCodeAt(0) - "A".charCodeAt(0) + 1), 0);
95+
96+
return { row, column };
97+
}
8298

8399
const createCellElement = (doc: Document, colIndex: number, rowIndex: number, data: string): Element => {
84100
const cell: Element = doc.createElementNS(doc.documentElement.namespaceURI, element.kindCell);
@@ -131,8 +147,8 @@ export default {
131147
getCellReferenceRelative,
132148
getCellReferenceAbsolute,
133149
createCell: createCellElement,
134-
getTableReference,
135150
updateCellData,
136151
resolveType,
137152
convertToExcelColumn,
153+
GetStartPosition,
138154
};

src/utils/tableUtils.ts

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44
import JSZip from "jszip";
55
import { TableData } from "../types";
66
import {
7-
defaults,
87
element,
98
elementAttributes,
109
queryTableNotFoundErr,
1110
queryTableXmlPath,
1211
sheetsNotFoundErr,
13-
sheetsXmlPath,
1412
tableNotFoundErr,
15-
tableXmlPath,
1613
textResultType,
1714
workbookXmlPath,
1815
xmlTextResultType,
@@ -21,18 +18,28 @@ import documentUtils from "./documentUtils";
2118
import { v4 } from "uuid";
2219
import { DOMParser, XMLSerializer } from "xmldom-qsa";
2320

24-
const updateTableInitialDataIfNeeded = async (zip: JSZip, tableData?: TableData, updateQueryTable?: boolean): Promise<void> => {
21+
/**
22+
* Update initial data for a table, its sheet, query table, and defined name if provided.
23+
* @param zip - The JSZip instance containing workbook parts.
24+
* @param cellRangeRef - Cell range reference (e.g. "A1:C5").
25+
* @param sheetPath - Path to the sheet XML within the zip.
26+
* @param tablePath - Path to the table XML within the zip.
27+
* @param tableName - Name of the table.
28+
* @param tableData - Optional TableData containing headers and rows.
29+
* @param updateQueryTable - Whether to update the associated queryTable part.
30+
*/
31+
const updateTableInitialDataIfNeeded = async (zip: JSZip, cellRangeRef: string, sheetPath: string, tablePath: string, sheetName: string, tableData?: TableData, updateQueryTable?: boolean): Promise<void> => {
2532
if (!tableData) {
2633
return;
2734
}
2835

29-
const sheetsXmlString: string | undefined = await zip.file(sheetsXmlPath)?.async(textResultType);
36+
const sheetsXmlString: string | undefined = await zip.file(sheetPath)?.async(textResultType);
3037
if (sheetsXmlString === undefined) {
3138
throw new Error(sheetsNotFoundErr);
3239
}
3340

34-
const newSheet: string = updateSheetsInitialData(sheetsXmlString, tableData);
35-
zip.file(sheetsXmlPath, newSheet);
41+
const newSheet: string = updateSheetsInitialData(sheetsXmlString, tableData, cellRangeRef);
42+
zip.file(sheetPath, newSheet);
3643

3744
if (updateQueryTable) {
3845
const queryTableXmlString: string | undefined = await zip.file(queryTableXmlPath)?.async(textResultType);
@@ -49,20 +56,28 @@ const updateTableInitialDataIfNeeded = async (zip: JSZip, tableData?: TableData,
4956
throw new Error(sheetsNotFoundErr);
5057
}
5158

52-
const newWorkbook: string = updateWorkbookInitialData(workbookXmlString, tableData);
59+
const newWorkbook: string = updateWorkbookInitialData(workbookXmlString, sheetName + GenerateReferenceFromString(cellRangeRef));
5360
zip.file(workbookXmlPath, newWorkbook);
5461
}
5562

56-
const tableXmlString: string | undefined = await zip.file(tableXmlPath)?.async(textResultType);
63+
const tableXmlString: string | undefined = await zip.file(tablePath)?.async(textResultType);
5764
if (tableXmlString === undefined) {
5865
throw new Error(tableNotFoundErr);
5966
}
6067

61-
const newTable: string = updateTablesInitialData(tableXmlString, tableData, updateQueryTable);
62-
zip.file(tableXmlPath, newTable);
68+
const newTable: string = updateTablesInitialData(tableXmlString, tableData, cellRangeRef, updateQueryTable);
69+
zip.file(tablePath, newTable);
6370
};
6471

65-
const updateTablesInitialData = (tableXmlString: string, tableData: TableData, updateQueryTable = false): string => {
72+
/**
73+
* Generate updated table XML string with new columns, reference, and filter range.
74+
* @param tableXmlString - Original table XML.
75+
* @param tableData - TableData containing column names.
76+
* @param cellRangeRef - Cell range reference.
77+
* @param updateQueryTable - Whether to include queryTable attributes.
78+
* @returns Serialized XML string of the updated table.
79+
*/
80+
const updateTablesInitialData = (tableXmlString: string, tableData: TableData, cellRangeRef: string, updateQueryTable = false): string => {
6681
const parser: DOMParser = new DOMParser();
6782
const serializer: XMLSerializer = new XMLSerializer();
6883
const tableDoc: Document = parser.parseFromString(tableXmlString, xmlTextResultType);
@@ -84,21 +99,26 @@ const updateTablesInitialData = (tableXmlString: string, tableData: TableData, u
8499
tableColumns.setAttribute(elementAttributes.count, tableData.columnNames.length.toString());
85100
tableDoc
86101
.getElementsByTagName(element.table)[0]
87-
.setAttribute(elementAttributes.reference, `A1:${documentUtils.getCellReferenceRelative(tableData.columnNames.length - 1, tableData.rows.length + 1)}`);
102+
.setAttribute(elementAttributes.reference, cellRangeRef);
88103
tableDoc
89104
.getElementsByTagName(element.autoFilter)[0]
90-
.setAttribute(elementAttributes.reference, `A1:${documentUtils.getCellReferenceRelative(tableData.columnNames.length - 1, tableData.rows.length + 1)}`);
105+
.setAttribute(elementAttributes.reference, cellRangeRef);
91106

92107
return serializer.serializeToString(tableDoc);
93108
};
94109

95-
const updateWorkbookInitialData = (workbookXmlString: string, tableData: TableData): string => {
110+
/**
111+
* Update the definedName element in workbook XML to a custom name.
112+
* @param workbookXmlString - Original workbook XML string.
113+
* @param customDefinedName - New defined name text content (e.g. "!$A$1:$C$5").
114+
* @returns Serialized XML string of the updated workbook.
115+
*/
116+
const updateWorkbookInitialData = (workbookXmlString: string, customDefinedName: string): string => {
96117
const newParser: DOMParser = new DOMParser();
97118
const newSerializer: XMLSerializer = new XMLSerializer();
98119
const workbookDoc: Document = newParser.parseFromString(workbookXmlString, xmlTextResultType);
99120
const definedName: Element = workbookDoc.getElementsByTagName(element.definedName)[0];
100-
definedName.textContent =
101-
defaults.sheetName + `!$A$1:${documentUtils.getCellReferenceAbsolute(tableData.columnNames.length - 1, tableData.rows.length + 1)}`;
121+
definedName.textContent = customDefinedName;
102122

103123
return newSerializer.serializeToString(workbookDoc);
104124
};
@@ -122,44 +142,69 @@ const updateQueryTablesInitialData = (queryTableXmlString: string, tableData: Ta
122142
return serializer.serializeToString(queryTableDoc);
123143
};
124144

125-
const updateSheetsInitialData = (sheetsXmlString: string, tableData: TableData): string => {
145+
/**
146+
* Update sheet XML with header row and data rows based on TableData.
147+
* @param sheetsXmlString - Original sheet XML string.
148+
* @param tableData - TableData containing headers and rows.
149+
* @param cellRangeRef - Cell range reference.
150+
* @returns Serialized XML string of the updated sheet.
151+
*/
152+
const updateSheetsInitialData = (sheetsXmlString: string, tableData: TableData, cellRangeRef: string): string => {
153+
let { row, column } = documentUtils.GetStartPosition(cellRangeRef);
126154
const parser: DOMParser = new DOMParser();
127155
const serializer: XMLSerializer = new XMLSerializer();
128156
const sheetsDoc: Document = parser.parseFromString(sheetsXmlString, xmlTextResultType);
129157
const sheetData: Element = sheetsDoc.getElementsByTagName(element.sheetData)[0];
130158
sheetData.textContent = "";
131-
let rowIndex = 0;
159+
132160
const columnRow: Element = sheetsDoc.createElementNS(sheetsDoc.documentElement.namespaceURI, element.row);
133-
columnRow.setAttribute(elementAttributes.row, (rowIndex + 1).toString());
134-
columnRow.setAttribute(elementAttributes.spans, "1:" + tableData.columnNames.length);
161+
columnRow.setAttribute(elementAttributes.row, row.toString());
162+
columnRow.setAttribute(elementAttributes.spans, column + ":" + (column + tableData.columnNames.length - 1));
135163
columnRow.setAttribute(elementAttributes.x14acDyDescent, "0.3");
136164
tableData.columnNames.forEach((col: string, colIndex: number) => {
137-
columnRow.appendChild(documentUtils.createCell(sheetsDoc, colIndex, rowIndex, col));
165+
columnRow.appendChild(documentUtils.createCell(sheetsDoc, colIndex + column - 1, row - 1, col));
138166
});
139167
sheetData.appendChild(columnRow);
140-
rowIndex++;
141-
tableData.rows.forEach((row) => {
168+
row++;
169+
170+
tableData.rows.forEach((_row) => {
142171
const newRow = sheetsDoc.createElementNS(sheetsDoc.documentElement.namespaceURI, element.row);
143-
newRow.setAttribute(elementAttributes.row, (rowIndex + 1).toString());
144-
newRow.setAttribute(elementAttributes.spans, "1:" + row.length);
172+
newRow.setAttribute(elementAttributes.row, row.toString());
173+
newRow.setAttribute(elementAttributes.spans, column + ":" + (column + tableData.columnNames.length - 1));
145174
newRow.setAttribute(elementAttributes.x14acDyDescent, "0.3");
146-
row.forEach((cellContent, colIndex) => {
147-
newRow.appendChild(documentUtils.createCell(sheetsDoc, colIndex, rowIndex, cellContent));
175+
_row.forEach((cellContent, colIndex) => {
176+
newRow.appendChild(documentUtils.createCell(sheetsDoc, colIndex + column - 1, row - 1, cellContent));
148177
});
149178
sheetData.appendChild(newRow);
150-
rowIndex++;
179+
row++;
151180
});
152-
const reference = documentUtils.getTableReference(tableData.rows[0].length - 1, tableData.rows.length + 1);
153181

154-
sheetsDoc.getElementsByTagName(element.dimension)[0].setAttribute(elementAttributes.reference, reference);
155-
sheetsDoc.getElementsByTagName(element.selection)[0].setAttribute(elementAttributes.sqref, reference);
182+
sheetsDoc.getElementsByTagName(element.dimension)[0].setAttribute(elementAttributes.reference, cellRangeRef);
183+
sheetsDoc.getElementsByTagName(element.selection)[0].setAttribute(elementAttributes.sqref, cellRangeRef);
156184
return serializer.serializeToString(sheetsDoc);
157185
};
158186

187+
/**
188+
* Add Excel-style dollar signs and a '!' prefix to a cell range.
189+
* Converts "A1:B2" into "!$A$1:$B$2".
190+
* @param cellRangeRef - Range reference string without dollar signs.
191+
* @returns Range with dollar signs and prefix.
192+
*/
193+
const GenerateReferenceFromString = (cellRangeRef: string): string => {
194+
return "!" + cellRangeRef.split(":").map(part => {
195+
const match = part.match(/^([A-Za-z]+)(\d+)$/);
196+
if (match) {
197+
const [, col, row] = match;
198+
return `$${col.toUpperCase()}$${row}`;
199+
}
200+
}).join(":");
201+
}
202+
159203
export default {
160204
updateTableInitialDataIfNeeded,
161205
updateSheetsInitialData,
162206
updateWorkbookInitialData,
163207
updateTablesInitialData,
164208
updateQueryTablesInitialData,
209+
GenerateReferenceFromString,
165210
};

0 commit comments

Comments
 (0)