Compare commits

..

3 Commits

Author SHA1 Message Date
237f57cf36 drastically reduce time to create photo folders 2025-10-19 23:11:22 -06:00
a893cd326f automatically create photo folder 2025-09-30 00:10:40 -06:00
92f636f247 combine metafields update 2025-09-29 23:37:28 -06:00
5 changed files with 134 additions and 106 deletions

View File

@ -218,76 +218,41 @@ export class Product {
console.log(JSON.stringify(response, null, 2)) console.log(JSON.stringify(response, null, 2))
} }
// update all metafields // update all metafields
this.UpdateAllMetafields(shop) this.UpdateAllMetafields(shop);
// create product photo folder
this.CreatePhotoFolder();
} }
UpdateEbayCategoryMetafield(shop: Shop) { UpdateAllMetafields(shop: Shop) {
console.log("UpdateEbayCategoryMetafield()") console.log("UpdateAllMetafields()")
if (!this.shopify_id) { if (!this.shopify_id) {
console.log("Cannot update metafields without a Shopify Product ID.") console.log("Cannot update metafields without a Shopify Product ID.")
return return
} }
if (this.product_type == "") {
console.log("No product type has been set. Skipping.")
return
}
this.ebay_category_id = this.EbayCategory()
if (this.ebay_category_id == "") {
console.log(`No eBay category defined for '${this.category}'`)
return
}
const metafieldsToSet: shopify.MetafieldsSetInput[] = [] const metafieldsToSet: shopify.MetafieldsSetInput[] = []
metafieldsToSet.push({
key: "ebay_category_id",
namespace: "custom",
ownerId: this.shopify_id,
type: "single_line_text_field",
value: this.ebay_category_id.toString(),
})
const query = /* GraphQL */ ` // eBay Category Metafield
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) { if (this.product_type) {
metafieldsSet(metafields: $metafields) { this.ebay_category_id = this.EbayCategory()
metafields { if (this.ebay_category_id) {
id metafieldsToSet.push({
key key: "ebay_category_id",
namespace namespace: "custom",
value ownerId: this.shopify_id,
} type: "single_line_text_field",
userErrors { value: this.ebay_category_id.toString(),
field })
message } else {
code console.log(
} `No eBay category defined for product type '${this.product_type}'`
} )
} }
` } else {
console.log("No product type set, skipping eBay category metafield.")
const variables = {
metafields: metafieldsToSet,
} }
const json = `{ // Dimension Metafields
"query": ${formatGqlForJSON(String(query))},
"variables": ${JSON.stringify(variables)}
}`
console.log("Setting ebay_category_id metafield with query:\n" + json)
const response = shop.shopifyGraphQLAPI(JSON.parse(json))
console.log("metafieldsSet response: " + JSON.stringify(response, null, 2))
}
// TODO: Make this a Product class method?
UpdateDimensionMetafields(shop: Shop) {
console.log("UpdateDimensionMetafields()")
if (!this.shopify_id) {
console.log("Cannot update metafields without a Shopify Product ID.")
return
}
const metafieldsToSet: shopify.MetafieldsSetInput[] = []
if (this.product_height_cm > 0) { if (this.product_height_cm > 0) {
metafieldsToSet.push({ metafieldsToSet.push({
key: "product_height_cm", key: "product_height_cm",
@ -328,7 +293,7 @@ export class Product {
} }
if (metafieldsToSet.length === 0) { if (metafieldsToSet.length === 0) {
console.log("No dimension metafields to update.") console.log("No metafields to update.")
return return
} }
@ -359,11 +324,16 @@ export class Product {
"variables": ${JSON.stringify(variables)} "variables": ${JSON.stringify(variables)}
}` }`
console.log("Setting dimension metafields with query:\n" + json) console.log("Setting metafields with query:\n" + json)
const response = shop.shopifyGraphQLAPI(JSON.parse(json)) const response = shop.shopifyGraphQLAPI(JSON.parse(json))
console.log("metafieldsSet response: " + JSON.stringify(response, null, 2)) console.log("metafieldsSet response: " + JSON.stringify(response, null, 2))
} }
CreatePhotoFolder() {
console.log("Product.CreatePhotoFolder()");
createPhotoFolderForSku(new(Config), this.sku);
}
PublishToShopifyOnlineStore(shop: Shop) { PublishToShopifyOnlineStore(shop: Shop) {
console.log("PublishToShopifyOnlineStore") console.log("PublishToShopifyOnlineStore")
let config = new Config() let config = new Config()
@ -408,3 +378,56 @@ export class Product {
// TODO: shopify_status // TODO: shopify_status
} }
} }
export function createPhotoFolderForSku(config: Config, sku: string) {
console.log(`createPhotoFolderForSku('${sku}')`)
if (!config.productPhotosFolderId) {
console.log(
"productPhotoFolderId not set in config. Skipping folder creation."
)
return
}
const productInventorySheet =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
const row = getRowByColumnValue("product_inventory", "sku", sku)
if (!row) {
console.log(`SKU '${sku}' not found in sheet. Cannot create folder.`)
return
}
const photosCell = getCellRangeByColumnName(
productInventorySheet,
"photos",
row
)
const folderUrl = photosCell.getRichTextValue().getLinkUrl()
console.log(`Folder URL from cell: ${folderUrl}`)
if (folderUrl && folderUrl.includes("drive.google.com")) {
console.log(`Photo folder already exists: ${folderUrl}`)
return
} else {
console.log(`Creating photo folder for SKU: ${sku}`)
}
const parentFolder = DriveApp.getFolderById(config.productPhotosFolderId)
const folderName = sku
let newFolder: GoogleAppsScript.Drive.Folder
const existingFolders = parentFolder.getFoldersByName(folderName)
if (existingFolders.hasNext()) {
newFolder = existingFolders.next()
console.log(`Found existing photo folder: '${folderName}'`)
} else {
newFolder = parentFolder.createFolder(folderName)
console.log(`Created new photo folder: '${folderName}'`)
}
let url = newFolder.getUrl()
console.log(`Folder URL: ${url}`)
let linkValue = SpreadsheetApp.newRichTextValue()
.setText(folderName)
.setLinkUrl(url)
.build()
photosCell.setRichTextValue(linkValue)
}

View File

@ -1,56 +1,32 @@
import { createPhotoFolderForSku } from "./Product"
import { getColumnRichTextByName, getColumnValuesByName, toastAndLog } from "./sheetUtils"
import { Config } from "./config" import { Config } from "./config"
import {
getCellRangeByColumnName,
getColumnValuesByName,
toastAndLog,
} from "./sheetUtils"
export function createMissingPhotoFolders() { export function createMissingPhotoFolders() {
let ss = SpreadsheetApp.getActive() const ss = SpreadsheetApp.getActive()
let s = ss.getSheetByName("product_inventory") const s = ss.getSheetByName("product_inventory")
let config = new Config() if (!s) {
let photoParent = DriveApp.getFolderById(config.productPhotosFolderId) toastAndLog("Could not find 'product_inventory' sheet.")
return
}
let skus = getColumnValuesByName(s, "sku") let skus = getColumnValuesByName(s, "sku")
let photoLinks = getColumnValuesByName(s, "photos") let photos = getColumnRichTextByName(s, "photos")
let created: string[] = [] let config = new Config()
let folderItr = photoParent.getFolders() // Process rows backward, as that is where the missing folders are most likely to occur
let folderNames: string[] = [] for (let i = skus.length - 1; i >= 0; i--) {
console.log("getting list of existing folders...") const sku = String(skus[i][0])
while (folderItr.hasNext()) { if (!sku) {
let folder = folderItr.next()
folderNames.push(folder.getName())
}
console.log("existing folders: " + folderNames.join(", "))
for (let i = 0; i < skus.length; i++) {
let sku = String(skus[i][0])
let updateLink: boolean = false
if (null == sku || sku == "") {
continue continue
} }
if (folderNames.includes(sku)) { let folderUrl = photos[i][0].getLinkUrl()
console.log("folder '" + sku + "' already exists") if (folderUrl && folderUrl.includes("drive.google.com")) {
} else { console.log(`Photo folder already exists for SKU: ${sku}`)
console.log("creating folder '" + skus[i] + "'")
photoParent.createFolder(sku)
created.push(sku)
updateLink = true
}
// Update photos cell
if (photoLinks[i][0] != "" && !updateLink) {
continue continue
} }
console.log("updating photos cell for '" + sku + "'")
let photosCell = getCellRangeByColumnName(s, "photos", i + 2) createPhotoFolderForSku(config, sku)
let folder = photoParent.getFoldersByName(sku).next()
let url = folder.getUrl()
let linkValue = SpreadsheetApp.newRichTextValue()
.setText(sku)
.setLinkUrl(url)
.build()
photosCell.setRichTextValue(linkValue)
} }
toastAndLog("created " + created.length + " folders: " + created.join(", ")) toastAndLog("Finished creating missing photo folders.")
} }

View File

@ -1,3 +1,5 @@
import { createPhotoFolderForSku } from "./Product"
import { Config } from "./config"
import { import {
getColumnByName, getColumnByName,
getCellRangeByColumnName, getCellRangeByColumnName,
@ -24,8 +26,13 @@ export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
// Acquire a user lock to prevent multiple onEdit calls from clashing // Acquire a user lock to prevent multiple onEdit calls from clashing
const documentLock = LockService.getDocumentLock() const documentLock = LockService.getDocumentLock()
try { try {
const config = new (Config);
documentLock.waitLock(LOCK_TIMEOUT_MS) documentLock.waitLock(LOCK_TIMEOUT_MS)
newSku(row) const sku = newSku(row)
console.log("new sku: " + sku)
createPhotoFolderForSku(config, String(sku))
} catch (error) {
console.log("Error in newSkuHandler: " + error.message)
} finally { } finally {
documentLock.releaseLock() documentLock.releaseLock()
} }
@ -70,4 +77,6 @@ export function newSku(row: number) {
let newId = maxId + 1 let newId = maxId + 1
console.log("newId: " + newId) console.log("newId: " + newId)
idCell.setValue(newId) idCell.setValue(newId)
return `${skuPrefixCellValue}-${newId.toString().padStart(4, "0")}`
} }

View File

@ -22,6 +22,11 @@ export function onEditQueue(e) {
console.log("No SKU found for row " + row) console.log("No SKU found for row " + row)
return return
} }
// Make sure SKU conforms to expected patterns
if (sku.match(`\\?`) || sku.match(`n$`)) {
console.log("SKU is a placeholder ('?' or 'n...'), skipping batching.")
return
}
// Acquire a user lock to prevent multiple onEdit calls from clashing // Acquire a user lock to prevent multiple onEdit calls from clashing
const documentLock = LockService.getDocumentLock() const documentLock = LockService.getDocumentLock()
try { try {
@ -85,6 +90,11 @@ export function processBatchedEdits() {
console.log( console.log(
`Processing SKU ${edit.sku}, Timestamp: ${new Date(edit.timestamp)}` `Processing SKU ${edit.sku}, Timestamp: ${new Date(edit.timestamp)}`
) )
// Make sure SKU conforms to expected patterns
if (!edit.sku.match(/^\w+-\d{4}$/)) {
console.log(`SKU ${edit.sku} is not valid, skipping processing.`)
return
}
let p = new Product(edit.sku) let p = new Product(edit.sku)
p.UpdateShopifyProduct(shop) p.UpdateShopifyProduct(shop)
}) })

View File

@ -50,6 +50,16 @@ export function getColumnValuesByName(
} }
} }
export function getColumnRichTextByName(
sheet: GoogleAppsScript.Spreadsheet.Sheet,
columnName: string
) {
let column = getColumnRangeByName(sheet, columnName)
if (column != null) {
return column.getRichTextValues()
}
}
export function vlookupByColumns( export function vlookupByColumns(
sheetName: string, sheetName: string,
searchColumn: string, searchColumn: string,