feat: Start refactoring code base to be testable
Implement a spreadsheet service abstraction, GAS integration, and Jest testing setup.
This commit is contained in:
6
jest.config.js
Normal file
6
jest.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
setupFiles: ['<rootDir>/src/test/setup.ts'],
|
||||||
|
};
|
||||||
4734
package-lock.json
generated
4734
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,15 +7,20 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"deploy": "clasp push"
|
"deploy": "clasp push",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/google-apps-script": "^1.0.85",
|
"@types/google-apps-script": "^1.0.85",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"copy-webpack-plugin": "^13.0.1",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"gas-webpack-plugin": "^2.6.0",
|
"gas-webpack-plugin": "^2.6.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings",
|
"shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings",
|
||||||
|
"ts-jest": "^29.4.6",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"webpack": "^5.96.1",
|
"webpack": "^5.96.1",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
35
src/Product.test.ts
Normal file
35
src/Product.test.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Product } from "./Product";
|
||||||
|
import { MockSpreadsheetService } from "./services/MockSpreadsheetService";
|
||||||
|
|
||||||
|
describe("Product", () => {
|
||||||
|
let mockService: MockSpreadsheetService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Setup mock data
|
||||||
|
const headers = ["sku", "title", "price", "weight_grams"];
|
||||||
|
const productData = ["TEST-SKU-1", "Test Product", 10.99, 500];
|
||||||
|
|
||||||
|
// product_inventory sheet: Row 1 = headers, Row 2 = data
|
||||||
|
mockService = new MockSpreadsheetService({
|
||||||
|
product_inventory: [
|
||||||
|
headers,
|
||||||
|
productData
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should load data from inventory sheet using service", () => {
|
||||||
|
const product = new Product("TEST-SKU-1", mockService);
|
||||||
|
|
||||||
|
expect(product.sku).toBe("TEST-SKU-1");
|
||||||
|
expect(product.title).toBe("Test Product");
|
||||||
|
expect(product.price).toBe(10.99);
|
||||||
|
expect(product.weight_grams).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if SKU not found", () => {
|
||||||
|
expect(() => {
|
||||||
|
new Product("NON-EXISTENT-SKU", mockService);
|
||||||
|
}).toThrow("product sku 'NON-EXISTENT-SKU' not found in product_inventory");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -8,16 +8,12 @@ import {
|
|||||||
ShopifyProductVariant,
|
ShopifyProductVariant,
|
||||||
ShopifyProductSetQuery,
|
ShopifyProductSetQuery,
|
||||||
ShopifyVariant,
|
ShopifyVariant,
|
||||||
VariantOptionValueInput,
|
|
||||||
formatGqlForJSON,
|
formatGqlForJSON,
|
||||||
} from "./shopifyApi"
|
} from "./shopifyApi"
|
||||||
import * as shopify from "shopify-admin-api-typings"
|
import * as shopify from "shopify-admin-api-typings"
|
||||||
import {
|
|
||||||
getCellRangeByColumnName,
|
|
||||||
getRowByColumnValue,
|
|
||||||
vlookupByColumns,
|
|
||||||
} from "./sheetUtils"
|
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
|
import { ISpreadsheetService } from "./interfaces/ISpreadsheetService"
|
||||||
|
import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
|
||||||
|
|
||||||
export class Product {
|
export class Product {
|
||||||
shopify_id: string = ""
|
shopify_id: string = ""
|
||||||
@ -46,12 +42,15 @@ export class Product {
|
|||||||
shopify_default_option_value_id: string = ""
|
shopify_default_option_value_id: string = ""
|
||||||
shopify_status: string = ""
|
shopify_status: string = ""
|
||||||
|
|
||||||
constructor(sku: string = "") {
|
private sheetService: ISpreadsheetService
|
||||||
|
|
||||||
|
constructor(sku: string = "", sheetService: ISpreadsheetService = new GASSpreadsheetService()) {
|
||||||
|
this.sheetService = sheetService;
|
||||||
if (sku == "") {
|
if (sku == "") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.sku = sku
|
this.sku = sku
|
||||||
let productRow = getRowByColumnValue("product_inventory", "sku", sku)
|
let productRow = this.sheetService.getRowNumberByColumnValue("product_inventory", "sku", sku)
|
||||||
if (productRow == undefined) {
|
if (productRow == undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"product sku '" + sku + "' not found in product_inventory"
|
"product sku '" + sku + "' not found in product_inventory"
|
||||||
@ -61,15 +60,10 @@ export class Product {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImportFromInventory(row: number) {
|
ImportFromInventory(row: number) {
|
||||||
let productInventorySheet =
|
let headers = this.sheetService.getHeaders("product_inventory")
|
||||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
|
||||||
let headers = productInventorySheet
|
|
||||||
.getRange(1, 1, 1, productInventorySheet.getLastColumn())
|
|
||||||
.getValues()[0]
|
|
||||||
console.log("headers" + headers)
|
console.log("headers" + headers)
|
||||||
let productValues = productInventorySheet
|
let productValues = this.sheetService.getRowData("product_inventory", row)
|
||||||
.getRange(row, 1, 1, headers.length)
|
console.log("productValues:" + productValues)
|
||||||
.getValues()[0]
|
|
||||||
console.log("productValues:" + productValues)
|
console.log("productValues:" + productValues)
|
||||||
for (let i = 0; i < headers.length; i++) {
|
for (let i = 0; i < headers.length; i++) {
|
||||||
if (this.hasOwnProperty(headers[i])) {
|
if (this.hasOwnProperty(headers[i])) {
|
||||||
@ -110,31 +104,26 @@ export class Product {
|
|||||||
this.shopify_id = this.shopify_product.id.toString()
|
this.shopify_id = this.shopify_product.id.toString()
|
||||||
this.shopify_default_variant_id = product.variants.nodes[0].id
|
this.shopify_default_variant_id = product.variants.nodes[0].id
|
||||||
this.shopify_default_option_id = product.options[0].id
|
this.shopify_default_option_id = product.options[0].id
|
||||||
|
this.shopify_default_option_id = product.options[0].id
|
||||||
this.shopify_default_option_value_id = product.options[0].optionValues[0].id
|
this.shopify_default_option_value_id = product.options[0].optionValues[0].id
|
||||||
let productInventorySheet =
|
let row = this.sheetService.getRowNumberByColumnValue("product_inventory", "sku", this.sku)
|
||||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
if (row) {
|
||||||
let row = getRowByColumnValue("product_inventory", "sku", this.sku)
|
this.sheetService.setCellValueByColumnName("product_inventory", row, "shopify_id", this.shopify_id)
|
||||||
getCellRangeByColumnName(productInventorySheet, "shopify_id", row).setValue(
|
}
|
||||||
this.shopify_id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ShopifyCategory(): string {
|
ShopifyCategory(): string {
|
||||||
return vlookupByColumns(
|
return this.sheetService.getCellValueByColumnName("values",
|
||||||
"values",
|
this.sheetService.getRowNumberByColumnValue("values", "product_type", this.product_type) || 0,
|
||||||
"product_type",
|
|
||||||
this.product_type,
|
|
||||||
"shopify_category"
|
"shopify_category"
|
||||||
)
|
) || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
EbayCategory(): string {
|
EbayCategory(): string {
|
||||||
return vlookupByColumns(
|
return this.sheetService.getCellValueByColumnName("values",
|
||||||
"values",
|
this.sheetService.getRowNumberByColumnValue("values", "product_type", this.product_type) || 0,
|
||||||
"product_type",
|
|
||||||
this.product_type,
|
|
||||||
"ebay_category_id"
|
"ebay_category_id"
|
||||||
)
|
) || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
ToShopifyProductSet() {
|
ToShopifyProductSet() {
|
||||||
@ -188,12 +177,11 @@ export class Product {
|
|||||||
let response = shop.shopifyGraphQLAPI(query.JSON)
|
let response = shop.shopifyGraphQLAPI(query.JSON)
|
||||||
let product = response.content.data.productSet.product
|
let product = response.content.data.productSet.product
|
||||||
this.shopify_id = product.id
|
this.shopify_id = product.id
|
||||||
let productInventorySheet =
|
|
||||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
let row = this.sheetService.getRowNumberByColumnValue("product_inventory", "sku", this.sku)
|
||||||
let row = getRowByColumnValue("product_inventory", "sku", this.sku)
|
if (row) {
|
||||||
getCellRangeByColumnName(productInventorySheet, "shopify_id", row).setValue(
|
this.sheetService.setCellValueByColumnName("product_inventory", row, "shopify_id", this.shopify_id)
|
||||||
this.shopify_id
|
}
|
||||||
)
|
|
||||||
let item: shopify.InventoryItem
|
let item: shopify.InventoryItem
|
||||||
do {
|
do {
|
||||||
console.log("UpdateShopifyProduct: attempting to get inventory item")
|
console.log("UpdateShopifyProduct: attempting to get inventory item")
|
||||||
@ -335,7 +323,7 @@ export class Product {
|
|||||||
|
|
||||||
CreatePhotoFolder() {
|
CreatePhotoFolder() {
|
||||||
console.log("Product.CreatePhotoFolder()");
|
console.log("Product.CreatePhotoFolder()");
|
||||||
createPhotoFolderForSku(new(Config), this.sku);
|
createPhotoFolderForSku(new(Config), this.sku, this.sheetService);
|
||||||
}
|
}
|
||||||
|
|
||||||
PublishToShopifyOnlineStore(shop: Shop) {
|
PublishToShopifyOnlineStore(shop: Shop) {
|
||||||
@ -383,7 +371,7 @@ export class Product {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPhotoFolderForSku(config: Config, sku: string) {
|
export function createPhotoFolderForSku(config: Config, sku: string, sheetService: ISpreadsheetService = new GASSpreadsheetService()) {
|
||||||
console.log(`createPhotoFolderForSku('${sku}')`)
|
console.log(`createPhotoFolderForSku('${sku}')`)
|
||||||
if (!config.productPhotosFolderId) {
|
if (!config.productPhotosFolderId) {
|
||||||
console.log(
|
console.log(
|
||||||
@ -392,19 +380,13 @@ export function createPhotoFolderForSku(config: Config, sku: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const productInventorySheet =
|
const row = sheetService.getRowNumberByColumnValue("product_inventory", "sku", sku)
|
||||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
|
||||||
const row = getRowByColumnValue("product_inventory", "sku", sku)
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
console.log(`SKU '${sku}' not found in sheet. Cannot create folder.`)
|
console.log(`SKU '${sku}' not found in sheet. Cannot create folder.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const photosCell = getCellRangeByColumnName(
|
|
||||||
productInventorySheet,
|
const folderUrl = sheetService.getCellHyperlink("product_inventory", row, "photos")
|
||||||
"photos",
|
|
||||||
row
|
|
||||||
)
|
|
||||||
const folderUrl = photosCell.getRichTextValue().getLinkUrl()
|
|
||||||
console.log(`Folder URL from cell: ${folderUrl}`)
|
console.log(`Folder URL from cell: ${folderUrl}`)
|
||||||
|
|
||||||
if (folderUrl && folderUrl.includes("drive.google.com")) {
|
if (folderUrl && folderUrl.includes("drive.google.com")) {
|
||||||
@ -429,9 +411,5 @@ export function createPhotoFolderForSku(config: Config, sku: string) {
|
|||||||
let url = newFolder.getUrl()
|
let url = newFolder.getUrl()
|
||||||
console.log(`Folder URL: ${url}`)
|
console.log(`Folder URL: ${url}`)
|
||||||
|
|
||||||
let linkValue = SpreadsheetApp.newRichTextValue()
|
sheetService.setCellHyperlink("product_inventory", row, "photos", folderName, url)
|
||||||
.setText(folderName)
|
|
||||||
.setLinkUrl(url)
|
|
||||||
.build()
|
|
||||||
photosCell.setRichTextValue(linkValue)
|
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/interfaces/ISpreadsheetService.ts
Normal file
9
src/interfaces/ISpreadsheetService.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface ISpreadsheetService {
|
||||||
|
getHeaders(sheetName: string): string[];
|
||||||
|
getRowData(sheetName: string, row: number): any[];
|
||||||
|
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined;
|
||||||
|
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void;
|
||||||
|
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any;
|
||||||
|
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null;
|
||||||
|
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void;
|
||||||
|
}
|
||||||
85
src/services/GASSpreadsheetService.ts
Normal file
85
src/services/GASSpreadsheetService.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
|
||||||
|
|
||||||
|
export class GASSpreadsheetService implements ISpreadsheetService {
|
||||||
|
private getSheet(sheetName: string): GoogleAppsScript.Spreadsheet.Sheet {
|
||||||
|
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
|
||||||
|
if (!sheet) {
|
||||||
|
throw new Error(`Sheet '${sheetName}' not found`);
|
||||||
|
}
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getColumnIndex(sheet: GoogleAppsScript.Spreadsheet.Sheet, columnName: string): number {
|
||||||
|
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
||||||
|
return headers.indexOf(columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeaders(sheetName: string): string[] {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
return sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowData(sheetName: string, row: number): any[] {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
const lastCol = sheet.getLastColumn();
|
||||||
|
// getRange(row, column, numRows, numColumns)
|
||||||
|
return sheet.getRange(row, 1, 1, lastCol).getValues()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||||
|
if (colIndex === -1) return undefined;
|
||||||
|
|
||||||
|
// Get all values in the column. Note: calling getValues() on a large sheet might be slow,
|
||||||
|
// but this matches the previous implementation's performance characteristics more or less.
|
||||||
|
// Ideally we would cache this or use a more efficient find.
|
||||||
|
// offset(1, colIndex) to skip header, but actually getColumnValuesByName usually gets everything including header or handles it.
|
||||||
|
// Original implementation: getColumnValuesByName gets range from row 2.
|
||||||
|
|
||||||
|
const data = sheet.getRange(2, colIndex + 1, sheet.getLastRow() - 1, 1).getValues();
|
||||||
|
const flatData = data.map(r => r[0]);
|
||||||
|
const index = flatData.indexOf(value);
|
||||||
|
|
||||||
|
if (index === -1) return undefined;
|
||||||
|
return index + 2; // +1 for 0-based index, +1 for header row
|
||||||
|
}
|
||||||
|
|
||||||
|
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||||
|
if (colIndex !== -1) {
|
||||||
|
sheet.getRange(row, colIndex + 1).setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||||
|
if (colIndex !== -1) {
|
||||||
|
return sheet.getRange(row, colIndex + 1).getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||||
|
if (colIndex !== -1) {
|
||||||
|
return sheet.getRange(row, colIndex + 1).getRichTextValue().getLinkUrl();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
|
||||||
|
const sheet = this.getSheet(sheetName);
|
||||||
|
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||||
|
if (colIndex !== -1) {
|
||||||
|
const richText = SpreadsheetApp.newRichTextValue()
|
||||||
|
.setText(displayText)
|
||||||
|
.setLinkUrl(url)
|
||||||
|
.build();
|
||||||
|
sheet.getRange(row, colIndex + 1).setRichTextValue(richText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/services/MockSpreadsheetService.ts
Normal file
113
src/services/MockSpreadsheetService.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
|
||||||
|
|
||||||
|
export class MockSpreadsheetService implements ISpreadsheetService {
|
||||||
|
// Store data as a map of sheetName -> array of rows (arrays of values)
|
||||||
|
// Row 0 is headers.
|
||||||
|
private sheets: Map<string, any[][]> = new Map();
|
||||||
|
|
||||||
|
constructor(initialData?: { [sheetName: string]: any[][] }) {
|
||||||
|
if (initialData) {
|
||||||
|
for (const [name, rows] of Object.entries(initialData)) {
|
||||||
|
this.sheets.set(name, JSON.parse(JSON.stringify(rows))); // Deep copy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSheetData(sheetName: string, data: any[][]) {
|
||||||
|
this.sheets.set(sheetName, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSheet(sheetName: string): any[][] {
|
||||||
|
if (!this.sheets.has(sheetName)) {
|
||||||
|
// Create empty sheet with no headers if accessed but not defined?
|
||||||
|
// Or throw error to mimic GAS?
|
||||||
|
// Let's return empty array or throw.
|
||||||
|
throw new Error(`Sheet '${sheetName}' not found in mock`);
|
||||||
|
}
|
||||||
|
return this.sheets.get(sheetName)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeaders(sheetName: string): string[] {
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
if (data.length === 0) return [];
|
||||||
|
return data[0] as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowData(sheetName: string, row: number): any[] {
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
// Row is 1-based index
|
||||||
|
if (row > data.length || row < 1) {
|
||||||
|
throw new Error(`Row ${row} out of bounds`);
|
||||||
|
}
|
||||||
|
return data[row - 1]; // Convert to 0-based
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
if (data.length < 2) return undefined; // Only headers or empty
|
||||||
|
|
||||||
|
const headers = data[0];
|
||||||
|
const colIndex = headers.indexOf(columnName);
|
||||||
|
if (colIndex === -1) return undefined;
|
||||||
|
|
||||||
|
for (let i = 1; i < data.length; i++) {
|
||||||
|
if (data[i][colIndex] === value) {
|
||||||
|
return i + 1; // Convert 0-based index to 1-based row number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
const headers = data[0];
|
||||||
|
const colIndex = headers.indexOf(columnName);
|
||||||
|
|
||||||
|
if (colIndex === -1) {
|
||||||
|
throw new Error(`Column '${columnName}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure row exists, extend if necessary (basic behavior)
|
||||||
|
while (data.length < row) {
|
||||||
|
data.push(new Array(headers.length).fill(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
data[row - 1][colIndex] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
const headers = data[0];
|
||||||
|
const colIndex = headers.indexOf(columnName);
|
||||||
|
|
||||||
|
if (colIndex === -1) return null;
|
||||||
|
if (row > data.length) return null;
|
||||||
|
|
||||||
|
return data[row - 1][colIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to store links: key = "sheetName:row:colIndex", value = url
|
||||||
|
private links: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
const colIndex = data[0].indexOf(columnName);
|
||||||
|
if (colIndex === -1) return null;
|
||||||
|
|
||||||
|
const key = `${sheetName}:${row}:${colIndex}`;
|
||||||
|
return this.links.get(key) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
|
||||||
|
// Set text value
|
||||||
|
this.setCellValueByColumnName(sheetName, row, columnName, displayText);
|
||||||
|
|
||||||
|
// Set link
|
||||||
|
const data = this.getSheet(sheetName);
|
||||||
|
const colIndex = data[0].indexOf(columnName);
|
||||||
|
if (colIndex !== -1) {
|
||||||
|
const key = `${sheetName}:${row}:${colIndex}`;
|
||||||
|
this.links.set(key, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/sheetUtils.test.ts
Normal file
5
src/sheetUtils.test.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
describe('sheetUtils', () => {
|
||||||
|
it('should be able to run tests', () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/test/setup.ts
Normal file
33
src/test/setup.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Mock global SpreadsheetApp to prevent crashes when importing legacy code
|
||||||
|
global.SpreadsheetApp = {
|
||||||
|
getActive: () => ({
|
||||||
|
getSheetByName: () => ({
|
||||||
|
getDataRange: () => ({
|
||||||
|
offset: () => ({
|
||||||
|
getValues: () => [],
|
||||||
|
clear: () => {},
|
||||||
|
}),
|
||||||
|
getValues: () => [],
|
||||||
|
}),
|
||||||
|
getLastRow: () => 0,
|
||||||
|
getRange: () => ({
|
||||||
|
getValues: () => [],
|
||||||
|
setValue: () => {},
|
||||||
|
setValues: () => {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getSpreadsheetTimeZone: () => "GMT",
|
||||||
|
}),
|
||||||
|
getActiveSpreadsheet: () => ({
|
||||||
|
getSheetByName: () => null,
|
||||||
|
getSpreadsheetTimeZone: () => "GMT",
|
||||||
|
}),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
global.Logger = {
|
||||||
|
log: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
global.Utilities = {
|
||||||
|
formatDate: () => "2025-01-01",
|
||||||
|
} as any;
|
||||||
Reference in New Issue
Block a user