omg this is working

This commit is contained in:
Ben Miller
2024-11-13 23:43:32 -07:00
parent fb86c9c96d
commit a5c0a1176d
22 changed files with 2152 additions and 53 deletions

View File

@ -1 +0,0 @@
{"scriptId":"1qGGz2kPw4eE0Tt2wk34PWoNF0xtvZHLxq1I1dzigNC4yXrTfgtxShnuq","rootDir":"."}

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
**/node_modules/** **/node_modules/**
dist/**
desktop.ini desktop.ini

View File

@ -1,7 +0,0 @@
{
"timeZone": "America/Denver",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}

1824
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,19 @@
{ {
"name": "product_inventory",
"version": "0.0.1",
"description": "",
"sideEffects": [
"global.ts"
],
"scripts": {
"build": "webpack --mode production",
"deploy": "clasp push -P ./dist"
},
"devDependencies": { "devDependencies": {
"@types/google-apps-script": "^1.0.85" "@types/google-apps-script": "^1.0.85",
"gas-webpack-plugin": "^2.6.0",
"ts-loader": "^9.5.1",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4"
} }
} }

29
src/OnEditHandler.ts Normal file
View File

@ -0,0 +1,29 @@
/// <reference types="@types/google-apps-script" />
import { newSkuHandler } from "./newSku"
import { getCellRangeByColumnName } from "./sheetUtils"
import { matchProductToShopify } from "./match"
export function onEditHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
newSkuHandler(e)
matchProductToShopifyOnEditHandler(e)
}
function matchProductToShopifyOnEditHandler(
e: GoogleAppsScript.Events.SheetsOnEdit
) {
var sheet = SpreadsheetApp.getActive().getActiveSheet()
if (sheet.getName() !== "product_inventory") {
console.log("skipping edit on sheet " + sheet.getName())
return
}
let row = e.range.getRowIndex()
let idCell = getCellRangeByColumnName(sheet, "shopify_id", row)
let idCellValue = idCell.getValue()
console.log("idCellValue = '" + idCellValue + "'")
if (idCellValue != "?" && idCellValue != "n") {
console.log("Shopify match was not requested, returning")
return
}
console.log("row: " + row)
matchProductToShopify(row)
}

5
src/OnOpenHandler.ts Normal file
View File

@ -0,0 +1,5 @@
import { initMenu } from "./initMenu"
export function onOpenHandler() {
initMenu()
}

90
src/Product.ts Normal file
View File

@ -0,0 +1,90 @@
// prettier-ignore
import { Shop, ShopifyProduct, ShopifyProductsQuery, ShopifyProductsResponse } from "./shopifyApi"
import { getCellRangeByColumnName, getRowByColumnValue } from "./sheetUtils"
export class Product {
shopify_id: string = ""
title: string = ""
style: string[] = []
tags: string = ""
category: string = ""
product_type: string = ""
description: string = ""
sku: string = ""
price: number = 0
shipping: number = 0
function: string = ""
type: string = ""
weight_grams: number = 0
photos: string = ""
shopify_product: ShopifyProduct
constructor(sku: string = "") {
if (sku == "") {
return
}
this.sku = sku
let productRow = getRowByColumnValue("product_inventory", "sku", sku)
if (productRow == undefined) {
throw new Error(
"product sku '" + sku + "' not found in product_inventory"
)
}
this.ImportFromInventory(productRow)
}
ImportFromInventory(row: number) {
let productInventorySheet =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
let headers = productInventorySheet
.getRange(1, 1, 1, productInventorySheet.getLastColumn())
.getValues()[0]
console.log("headers" + headers)
let productValues = productInventorySheet
.getRange(row, 1, 1, headers.length)
.getValues()[0]
console.log("productValues:" + productValues)
for (let i = 0; i < headers.length; i++) {
if (this.hasOwnProperty(headers[i])) {
console.log(
"setting value for '" + headers[i] + "' to '" + productValues[i] + "'"
)
this[headers[i]] = productValues[i]
} else {
console.log("skipping '" + headers[i] + "'")
}
}
}
MatchToShopifyProduct(shop: Shop) {
if (this.shopify_id.startsWith("gid://shopify/Product/")) {
return
}
let query = new ShopifyProductsQuery(
"sku:" + this.sku,
["id", "title"]
)
console.log(query.JSON)
let response = shop.shopifyGraphQLAPI(query.JSON)
console.log(response)
let productsResponse = new ShopifyProductsResponse(response.content)
if (productsResponse.products.edges.length <= 0) {
console.log("no products matched")
return
}
if (productsResponse.products.edges.length > 1) {
console.log("more than one product matched")
return
}
this.shopify_product = productsResponse.products.edges[0].node
this.shopify_id = this.shopify_product.id.toString()
let productInventorySheet =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
let row = getRowByColumnValue("product_inventory", "sku", this.sku)
getCellRangeByColumnName(productInventorySheet, "shopify_id", row).setValue(
this.shopify_id
)
}
}

View File

@ -1,3 +1,5 @@
import { vlookupByColumns } from "./sheetUtils"
export class Config { export class Config {
productPhotosFolderId: string productPhotosFolderId: string
shopifyApiKey: string shopifyApiKey: string

View File

@ -1,4 +1,11 @@
function createMissingPhotoFolders() { import { Config } from "./config"
import {
getCellRangeByColumnName,
getColumnValuesByName,
toastAndLog,
} from "./sheetUtils"
export function createMissingPhotoFolders() {
let ss = SpreadsheetApp.getActive() let ss = SpreadsheetApp.getActive()
let s = ss.getSheetByName("product_inventory") let s = ss.getSheetByName("product_inventory")
let config = new Config() let config = new Config()

View File

@ -1,4 +1,6 @@
function fillProductFromTemplate() { import { productTemplate } from "./productTemplate"
export function fillProductFromTemplate() {
let row = SpreadsheetApp.getCurrentCell().getRow() let row = SpreadsheetApp.getCurrentCell().getRow()
productTemplate(row) productTemplate(row)
} }

13
src/global.ts Normal file
View File

@ -0,0 +1,13 @@
/// <reference types="@types/google-apps-script" />
import { onOpen } from "./onOpen"
import { onEdit } from "./onEdit"
import { getShopifyProducts } from "./shopifyApi"
import { runShopifyOrders } from "./shopifyApi"
import { matchProductToShopifyHandler } from "./initMenu"
;(global as any).onOpen = () => onOpen()
;(global as any).onEdit = (e: GoogleAppsScript.Events.SheetsOnEdit) => onEdit(e)
;(global as any).getShopifyProducts = getShopifyProducts
;(global as any).runShopifyOrders = runShopifyOrders
;(global as any).matchProductToShopifyHandler = matchProductToShopifyHandler

View File

@ -1,22 +1,34 @@
import { Shop } from "./shopifyApi" import { getShopifyProducts, runShopifyOrders } from "./shopifyApi"
import { fillProductFromTemplate } from "./fillProductFromTemplate"
import { createMissingPhotoFolders } from "./createMissingPhotoFolders"
import { matchProductToShopify } from "./match"
function initMenu() { export function initMenu() {
let ui = SpreadsheetApp.getUi() let ui = SpreadsheetApp.getUi()
ui.createMenu("BLM") ui.createMenu("BLM")
.addItem("Fill out product from template", "fillProductFromTemplate") .addSubMenu(
.addItem("Create missing photo folders", "createMissingPhotoFolders") ui
.createMenu("This row...")
.addItem("Fill out product from template", fillProductFromTemplate.name)
.addItem("Match product to Shopify", matchProductToShopifyHandler.name)
)
.addSeparator() .addSeparator()
.addItem("Run Shopify Orders", "runShopifyOrders") .addSubMenu(
.addItem("Get Shopify Products", "getShopifyProducts") ui
.createMenu("Bulk operations...")
.addItem("Create missing photo folders", createMissingPhotoFolders.name)
.addItem("Run Shopify Orders", runShopifyOrders.name)
.addItem("Get Shopify Products", getShopifyProducts.name)
)
.addToUi() .addToUi()
} }
function runShopifyOrders() { export function matchProductToShopifyHandler() {
let shop = new Shop() var sheet = SpreadsheetApp.getActive().getActiveSheet()
shop.RunOrders() if (sheet.getName() !== "product_inventory") {
} console.log("skipping edit on sheet " + sheet.getName())
return
function getShopifyProducts() { }
let shop = new Shop() let row = SpreadsheetApp.getCurrentCell().getRow()
shop.GetProducts() matchProductToShopify(row)
} }

12
src/match.ts Normal file
View File

@ -0,0 +1,12 @@
import { Shop } from "./shopifyApi"
import { Product } from "./Product"
export function matchProductToShopify(row: number) {
console.log("row: " + row)
let product = new Product()
console.log(product)
product.ImportFromInventory(row)
console.log(product)
product.MatchToShopifyProduct(new Shop())
console.log(product)
}

View File

@ -1,4 +1,10 @@
function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) { import {
getCellRangeByColumnName,
getCellValueByColumnName,
} from "./sheetUtils"
export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
var sheet = SpreadsheetApp.getActive().getActiveSheet() var sheet = SpreadsheetApp.getActive().getActiveSheet()
if (sheet.getName() !== "product_inventory") { if (sheet.getName() !== "product_inventory") {
console.log("skipping edit on sheet " + sheet.getName()) console.log("skipping edit on sheet " + sheet.getName())
@ -15,7 +21,7 @@ function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
newSku(row) newSku(row)
} }
function newSku(row: number) { export function newSku(row: number) {
let sheet = SpreadsheetApp.getActive().getSheetByName("product_inventory") let sheet = SpreadsheetApp.getActive().getSheetByName("product_inventory")
let idCell = getCellRangeByColumnName(sheet, "#", row) let idCell = getCellRangeByColumnName(sheet, "#", row)
let safeToOverwrite: string[] = ["?", "n", ""] let safeToOverwrite: string[] = ["?", "n", ""]

View File

@ -1,5 +1,5 @@
/// <reference types="@types/google-apps-script" /> import { onEditHandler } from "./OnEditHandler"
function onEdit(e: GoogleAppsScript.Events.SheetsOnEdit) { export function onEdit(e: GoogleAppsScript.Events.SheetsOnEdit) {
newSkuHandler(e) onEditHandler(e)
} }

View File

@ -1,3 +1,5 @@
function onOpen() { import { initMenu } from "./initMenu"
export function onOpen() {
initMenu() initMenu()
} }

View File

@ -1,4 +1,12 @@
function productTemplate(row: number) { import { newSku } from "./newSku"
import {
getCellRangeByColumnName,
getCellValueByColumnName,
toastAndLog,
vlookupByColumns,
} from "./sheetUtils"
export function productTemplate(row: number) {
let updateColumns = [ let updateColumns = [
"function", "function",
"type", "type",

View File

@ -1,4 +1,4 @@
function getCellRangeByColumnName( export function getCellRangeByColumnName(
sheet: GoogleAppsScript.Spreadsheet.Sheet, sheet: GoogleAppsScript.Spreadsheet.Sheet,
columnName: string, columnName: string,
row: number row: number
@ -10,7 +10,7 @@ function getCellRangeByColumnName(
} }
} }
function getCellValueByColumnName( export function getCellValueByColumnName(
sheet: GoogleAppsScript.Spreadsheet.Sheet, sheet: GoogleAppsScript.Spreadsheet.Sheet,
columnName: string, columnName: string,
row: number row: number
@ -21,7 +21,7 @@ function getCellValueByColumnName(
} }
} }
function getColumnRangeByName( export function getColumnRangeByName(
sheet: GoogleAppsScript.Spreadsheet.Sheet, sheet: GoogleAppsScript.Spreadsheet.Sheet,
columnName: string columnName: string
) { ) {
@ -32,7 +32,7 @@ function getColumnRangeByName(
} }
} }
function getColumnValuesByName( export function getColumnValuesByName(
sheet: GoogleAppsScript.Spreadsheet.Sheet, sheet: GoogleAppsScript.Spreadsheet.Sheet,
columnName: string columnName: string
) { ) {
@ -42,7 +42,7 @@ function getColumnValuesByName(
} }
} }
function vlookupByColumns( export function vlookupByColumns(
sheetName: string, sheetName: string,
searchColumn: string, searchColumn: string,
searchKey: string, searchKey: string,
@ -62,7 +62,26 @@ function vlookupByColumns(
return resultValue return resultValue
} }
function toastAndLog(message: string) { export function toastAndLog(message: string) {
SpreadsheetApp.getActive().toast(message) SpreadsheetApp.getActive().toast(message)
console.log(message) console.log(message)
} }
export function getRowByColumnValue(
sheetName: string,
columnName: string,
searchKey: string
) {
let s = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)
let searchData = getColumnValuesByName(s, columnName)
let dataList = searchData.map((x) => x[0])
let index = dataList.indexOf(searchKey)
if (index === -1) {
toastAndLog(searchKey + " not found")
return
}
let resultRow = index + 2
console.log("row found: " + resultRow)
return resultRow
}

View File

@ -482,18 +482,19 @@ export class Shop {
GetProducts() { GetProducts() {
let done = false let done = false
let query = ""
let cursor = "" let cursor = ""
let fields = ["id", "title"] let fields = ["id", "title"]
var response = { var response = {
content: {}, content: {},
headers: {}, headers: {},
} }
let products: Product[] = [] let products: ShopifyProduct[] = []
do { do {
let query = new ProductsQuery(fields, cursor) let pq = new ShopifyProductsQuery(query, fields, cursor)
response = this.shopifyGraphQLAPI(query.JSON) response = this.shopifyGraphQLAPI(pq.JSON)
console.log(response) console.log(response)
let productsResponse = new ProductsResponse(response.content) let productsResponse = new ShopifyProductsResponse(response.content)
if (productsResponse.products.edges.length <= 0) { if (productsResponse.products.edges.length <= 0) {
console.log("no products returned") console.log("no products returned")
done = true done = true
@ -502,7 +503,7 @@ export class Shop {
for (let i = 0; i < productsResponse.products.edges.length; i++) { for (let i = 0; i < productsResponse.products.edges.length; i++) {
let edge = productsResponse.products.edges[i] let edge = productsResponse.products.edges[i]
console.log(JSON.stringify(edge)) console.log(JSON.stringify(edge))
let p = new Product() let p = new ShopifyProduct()
Object.assign(edge.node, p) Object.assign(edge.node, p)
products.push(p) products.push(p)
} }
@ -609,7 +610,7 @@ export class Order {
} }
} }
export class Product { export class ShopifyProduct {
body_html: string body_html: string
created_at: Date created_at: Date
handle: string handle: string
@ -678,7 +679,7 @@ class Products {
} }
class ProductEdge { class ProductEdge {
node: Product node: ShopifyProduct
cursor: string cursor: string
} }
@ -689,12 +690,14 @@ class PageInfo {
endCursor: string endCursor: string
} }
class ProductsQuery { export class ShopifyProductsQuery {
GQL: string GQL: string
JSON: JSON JSON: JSON
constructor( constructor(
query: string = "",
fields: string[] = ["id", "title", "handle"], fields: string[] = ["id", "title", "handle"],
cursor: string = "" cursor: string = "",
pageSize: number = 10,
) { ) {
let cursorText: string let cursorText: string
if (cursor == "") { if (cursor == "") {
@ -702,8 +705,14 @@ class ProductsQuery {
} else { } else {
cursorText = `, after: "${cursor}"` cursorText = `, after: "${cursor}"`
} }
let queryText: string
if (query == "") {
queryText = ""
} else {
queryText = `, query: "${query}"`
}
this.GQL = `{ this.GQL = `{
products(first: 10${cursorText}) { products(first: ${pageSize}${cursorText}${queryText}) {
edges { edges {
node { ${fields.join(" ")} } node { ${fields.join(" ")} }
} }
@ -719,7 +728,7 @@ class ProductsQuery {
} }
} }
class ProductsResponse { export class ShopifyProductsResponse {
products: Products products: Products
constructor(response: {}) { constructor(response: {}) {
this.products = response["data"]["products"] this.products = response["data"]["products"]
@ -730,3 +739,15 @@ function formatGqlForJSON(gql: string) {
let singleLine = gql.split("\n").join(" ").replace(/\s+/g, " ") let singleLine = gql.split("\n").join(" ").replace(/\s+/g, " ")
return JSON.stringify(singleLine) return JSON.stringify(singleLine)
} }
export function runShopifyOrders() {
let shop = new Shop()
shop.RunOrders()
}
export function getShopifyProducts() {
let shop = new Shop()
shop.GetProducts()
}
(global as any)

View File

@ -1,5 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"lib": [ "lib": [
"esnext" "esnext"
], ],

40
webpack.config.js Normal file
View File

@ -0,0 +1,40 @@
const path = require('path');
const GasPlugin = require("gas-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: './src/global.ts',
optimization: {
minimize: false,
minimizer: [
new TerserPlugin({
terserOptions: {
keep_classnames: true,
keep_fnames: true
}
})
]
},
module: {
rules: [
{
test: /\.ts?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new GasPlugin({
comment: true,
autoGlobalExportsFiles: ['**/*.ts'],
})
]
};