Compare commits
5 Commits
a5f9b1542c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 237f57cf36 | |||
| a893cd326f | |||
| 92f636f247 | |||
| 66c711916e | |||
| 5b6db0eece |
127
src/Product.ts
127
src/Product.ts
@ -11,17 +11,21 @@ import {
|
|||||||
VariantOptionValueInput,
|
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 {
|
||||||
|
getCellRangeByColumnName,
|
||||||
|
getRowByColumnValue,
|
||||||
|
vlookupByColumns,
|
||||||
|
} from "./sheetUtils"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
|
|
||||||
|
|
||||||
export class Product {
|
export class Product {
|
||||||
shopify_id: string = ""
|
shopify_id: string = ""
|
||||||
title: string = ""
|
title: string = ""
|
||||||
style: string[] = []
|
style: string[] = []
|
||||||
tags: string = ""
|
tags: string = ""
|
||||||
category: string = ""
|
category: string = ""
|
||||||
|
ebay_category_id: string = ""
|
||||||
product_type: string = ""
|
product_type: string = ""
|
||||||
description: string = ""
|
description: string = ""
|
||||||
sku: string = ""
|
sku: string = ""
|
||||||
@ -115,7 +119,21 @@ export class Product {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShopifyCategory(): string {
|
ShopifyCategory(): string {
|
||||||
return vlookupByColumns("values", "product_type", this.product_type, "shopify_category")
|
return vlookupByColumns(
|
||||||
|
"values",
|
||||||
|
"product_type",
|
||||||
|
this.product_type,
|
||||||
|
"shopify_category"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EbayCategory(): string {
|
||||||
|
return vlookupByColumns(
|
||||||
|
"values",
|
||||||
|
"product_type",
|
||||||
|
this.product_type,
|
||||||
|
"ebay_category_id"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ToShopifyProductSet() {
|
ToShopifyProductSet() {
|
||||||
@ -186,7 +204,12 @@ export class Product {
|
|||||||
shop.SetInventoryItemDefaults(item, config)
|
shop.SetInventoryItemDefaults(item, config)
|
||||||
if (this.weight_grams > 0) {
|
if (this.weight_grams > 0) {
|
||||||
console.log("UpdateShopifyProduct: setting weight on inventory item")
|
console.log("UpdateShopifyProduct: setting weight on inventory item")
|
||||||
shop.SetInventoryItemWeight(item, config, this.weight_grams, shopify.WeightUnit.GRAMS)
|
shop.SetInventoryItemWeight(
|
||||||
|
item,
|
||||||
|
config,
|
||||||
|
this.weight_grams,
|
||||||
|
shopify.WeightUnit.GRAMS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (newProduct) {
|
if (newProduct) {
|
||||||
console.log("UpdateShopifyProduct: setting defaults on new product")
|
console.log("UpdateShopifyProduct: setting defaults on new product")
|
||||||
@ -194,13 +217,14 @@ export class Product {
|
|||||||
shop.UpdateInventoryItemQuantity(item, 1, config)
|
shop.UpdateInventoryItemQuantity(item, 1, config)
|
||||||
console.log(JSON.stringify(response, null, 2))
|
console.log(JSON.stringify(response, null, 2))
|
||||||
}
|
}
|
||||||
// update dimension metafields
|
// update all metafields
|
||||||
this.UpdateDimensionMetafields(shop)
|
this.UpdateAllMetafields(shop);
|
||||||
|
// create product photo folder
|
||||||
|
this.CreatePhotoFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this a Product class method?
|
UpdateAllMetafields(shop: Shop) {
|
||||||
UpdateDimensionMetafields(shop: Shop) {
|
console.log("UpdateAllMetafields()")
|
||||||
console.log("UpdateDimensionMetafields()")
|
|
||||||
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
|
||||||
@ -208,6 +232,27 @@ export class Product {
|
|||||||
|
|
||||||
const metafieldsToSet: shopify.MetafieldsSetInput[] = []
|
const metafieldsToSet: shopify.MetafieldsSetInput[] = []
|
||||||
|
|
||||||
|
// eBay Category Metafield
|
||||||
|
if (this.product_type) {
|
||||||
|
this.ebay_category_id = this.EbayCategory()
|
||||||
|
if (this.ebay_category_id) {
|
||||||
|
metafieldsToSet.push({
|
||||||
|
key: "ebay_category_id",
|
||||||
|
namespace: "custom",
|
||||||
|
ownerId: this.shopify_id,
|
||||||
|
type: "single_line_text_field",
|
||||||
|
value: this.ebay_category_id.toString(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`No eBay category defined for product type '${this.product_type}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No product type set, skipping eBay category metafield.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dimension Metafields
|
||||||
if (this.product_height_cm > 0) {
|
if (this.product_height_cm > 0) {
|
||||||
metafieldsToSet.push({
|
metafieldsToSet.push({
|
||||||
key: "product_height_cm",
|
key: "product_height_cm",
|
||||||
@ -248,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,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()
|
||||||
@ -328,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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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.")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user