Compare commits
5 Commits
243f7057b7
...
55d18138b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 55d18138b7 | |||
| 945fb610f9 | |||
| d67897aa17 | |||
| c738ab3ef7 | |||
| d9d884e1fc |
@ -40,5 +40,9 @@ This project (`product_inventory`) integrates Google Sheets with Shopify. It ser
|
||||
1. Sanitize with `Utilities.newBlob()`.
|
||||
2. Fallback to **Advanced Drive Service** (`Drive.Files.create` / `v3`) if standard creation fails.
|
||||
- **Video Previews**:
|
||||
- HTML5 `<video>` tags often fail with standard Drive download URLs due to auth/codec issues.
|
||||
- **Strategy**: Use an `<iframe>` embedding the `https://drive.google.com/file/d/{ID}/preview` URL. This leverages Google's native player for reliable auth and transcoding.
|
||||
- **Video Previews**:
|
||||
- Use `document.createElement('video')` to inject video tags. Avoid template strings (`<video src="...">`) as the parser sanitizes them aggressively.
|
||||
- Fallback to `<iframe>` only if native playback fails.
|
||||
- **Client-Side Syntax**:
|
||||
- **ES5 ONLY**: Do not use `class` in client-side HTML files. The Apps Script sanitizer often fails to parse them. Use `function` constructors.
|
||||
|
||||
|
||||
@ -141,3 +141,19 @@ We implemented a "Sidebar-First" architecture for product media to handle the co
|
||||
- Calculates checksums to avoid re-uploading duplicate images.
|
||||
- Uses Shopify's "Staged Uploads" -> "Create Media" mutation flow.
|
||||
|
||||
### 8. Apps Script & HTML Service Constraints
|
||||
|
||||
When working with `HtmlService` (client-side code), the environment differs significantly from the server-side V8 runtime.
|
||||
|
||||
1. **Server-Side (`.ts`/`.gs`)**:
|
||||
- **Runtime**: V8 Engine.
|
||||
- **Syntax**: Modern ES6+ (Classes, Arrow Functions, `const`/`let`) is fully supported.
|
||||
- **Recommendation**: Use standard TypeScript patterns.
|
||||
|
||||
2. **Client-Side (`.html` served via `createHtmlOutputFromFile`)**:
|
||||
- **Runtime**: Legacy Browser Environment / Strict Caja Sanitization.
|
||||
- **Constraint**: The parser often chokes on ES6 `class` syntax and complex template strings inside HTML attributes.
|
||||
- **Rule 1**: **NO ES6 CLASSES**. Use ES5 `function` constructors and `prototype` methods.
|
||||
- **Rule 2**: **NO Complex Template Strings in Attributes**. Do not use `src="${var}"` if the variable contains a URL. Use `document.createElement` and set properties (e.g., `element.src = value`) programmatically.
|
||||
- **Rule 3**: **Unified Script Tags**. Consolidate scripts into a single block where possible to avoid parser merge errors.
|
||||
- **Rule 4**: **Var over Let/Const**. Top-level variables should use `var` or explicit `window` assignment to ensure they are accessible to inline HTML handlers (e.g., `onclick="handler()"`).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
|
||||
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
|
||||
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
|
||||
import { installSalesSyncTrigger } from "./triggers"
|
||||
import { showMediaManager, getSelectedSku, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
|
||||
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
|
||||
import { runSystemDiagnostics } from "./verificationSuite"
|
||||
|
||||
// prettier-ignore
|
||||
@ -52,7 +52,7 @@ import { runSystemDiagnostics } from "./verificationSuite"
|
||||
;(global as any).reconcileSalesHandler = reconcileSalesHandler
|
||||
;(global as any).installSalesSyncTrigger = installSalesSyncTrigger
|
||||
;(global as any).showMediaManager = showMediaManager
|
||||
;(global as any).getSelectedSku = getSelectedSku
|
||||
;(global as any).getSelectedProductInfo = getSelectedProductInfo
|
||||
;(global as any).getMediaForSku = getMediaForSku
|
||||
;(global as any).saveFileToDrive = saveFileToDrive
|
||||
;(global as any).saveMediaChanges = saveMediaChanges
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
import { importFromPicker, getMediaForSku, createPhotoSession, checkPhotoSession, debugFolderAccess, showMediaManager, getSelectedSku, getPickerConfig, saveFileToDrive, debugScopes, saveMediaChanges } from "./mediaHandlers"
|
||||
import { importFromPicker, getMediaForSku, createPhotoSession, checkPhotoSession, debugFolderAccess, showMediaManager, getSelectedProductInfo, getPickerConfig, saveFileToDrive, debugScopes, saveMediaChanges } from "./mediaHandlers"
|
||||
import { Config } from "./config"
|
||||
import { GASDriveService } from "./services/GASDriveService"
|
||||
import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
|
||||
@ -47,7 +47,11 @@ jest.mock("./services/GASSpreadsheetService", () => {
|
||||
return {
|
||||
GASSpreadsheetService: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getCellValueByColumnName: jest.fn().mockReturnValue("TEST-SKU")
|
||||
getCellValueByColumnName: jest.fn().mockImplementation((sheet, row, col) => {
|
||||
if (col === "sku") return "TEST-SKU"
|
||||
if (col === "title") return "Test Product Title"
|
||||
return null
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -336,9 +340,9 @@ describe("mediaHandlers", () => {
|
||||
expect(mockUi.showModalDialog).toHaveBeenCalledWith(mockHtml, "Media Manager")
|
||||
})
|
||||
|
||||
test("getSelectedSku should return sku from sheet", () => {
|
||||
const sku = getSelectedSku()
|
||||
expect(sku).toBe("TEST-SKU")
|
||||
test("getSelectedProductInfo should return sku and title from sheet", () => {
|
||||
const info = getSelectedProductInfo()
|
||||
expect(info).toEqual({ sku: "TEST-SKU", title: "Test Product Title" })
|
||||
})
|
||||
|
||||
test("getPickerConfig should return config", () => {
|
||||
|
||||
@ -15,7 +15,7 @@ export function showMediaManager() {
|
||||
SpreadsheetApp.getUi().showModalDialog(html, "Media Manager");
|
||||
}
|
||||
|
||||
export function getSelectedSku(): string | null {
|
||||
export function getSelectedProductInfo(): { sku: string, title: string } | null {
|
||||
const ss = new GASSpreadsheetService()
|
||||
const sheet = SpreadsheetApp.getActiveSheet()
|
||||
if (sheet.getName() !== "product_inventory") return null
|
||||
@ -24,7 +24,9 @@ export function getSelectedSku(): string | null {
|
||||
if (row <= 1) return null // Header
|
||||
|
||||
const sku = ss.getCellValueByColumnName("product_inventory", row, "sku")
|
||||
return sku ? String(sku) : null
|
||||
const title = ss.getCellValueByColumnName("product_inventory", row, "title")
|
||||
|
||||
return sku ? { sku: String(sku), title: String(title || "") } : null
|
||||
}
|
||||
|
||||
export function getPickerConfig() {
|
||||
@ -105,7 +107,13 @@ export function getMediaDiagnostics(sku: string) {
|
||||
|
||||
const shopifyId = product.shopify_id || ""
|
||||
|
||||
return mediaService.getDiagnostics(sku, shopifyId)
|
||||
const diagnostics = mediaService.getDiagnostics(sku, shopifyId)
|
||||
|
||||
// Inject OAuth token for frontend video streaming (Drive API alt=media)
|
||||
return {
|
||||
...diagnostics,
|
||||
token: ScriptApp.getOAuthToken()
|
||||
}
|
||||
}
|
||||
|
||||
export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {
|
||||
|
||||
@ -139,6 +139,20 @@ export class MediaService {
|
||||
// Find Shopify Orphans
|
||||
shopifyMedia.forEach(m => {
|
||||
if (!matchedShopifyIds.has(m.id)) {
|
||||
let mimeType = 'image/jpeg'; // Default
|
||||
let contentUrl = "";
|
||||
|
||||
if (m.mediaContentType === 'VIDEO' && m.sources) {
|
||||
// Find MP4
|
||||
const mp4 = m.sources.find((s: any) => s.mimeType === 'video/mp4')
|
||||
if (mp4) {
|
||||
mimeType = mp4.mimeType
|
||||
contentUrl = mp4.url
|
||||
}
|
||||
} else if (m.mediaContentType === 'IMAGE' && m.image) {
|
||||
contentUrl = m.image.url
|
||||
}
|
||||
|
||||
unifiedState.push({
|
||||
id: m.id, // Use Shopify ID keys for orphans
|
||||
driveId: null,
|
||||
@ -148,7 +162,9 @@ export class MediaService {
|
||||
source: 'shopify_only',
|
||||
thumbnail: m.preview?.image?.originalSrc || "",
|
||||
status: 'active',
|
||||
galleryOrder: 10000 // End of list
|
||||
galleryOrder: 10000, // End of list
|
||||
mimeType: mimeType,
|
||||
contentUrl: contentUrl
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -78,6 +78,17 @@ export class ShopifyMediaService implements IShopifyMediaService {
|
||||
originalSrc
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
sources {
|
||||
url
|
||||
mimeType
|
||||
}
|
||||
}
|
||||
... on MediaImage {
|
||||
image {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user