Compare commits

..

5 Commits

Author SHA1 Message Date
55d18138b7 feat: handle missing SKU in Media Manager
- Added UI and logic to handle cases where the Media Manager is opened for a row without a SKU.
- Displays a user-friendly error message with a Close button.
- Fixed an issue where the Gallery card was not properly hidden in the error state.
2025-12-29 00:21:02 -07:00
945fb610f9 Fix: Prevent drag-drop overlay during internal reordering in Media Manager
Updated drag event listeners in MediaManager.html to check for 'Files' in dataTransfer.types. This ensures the upload overlay only appears when files are dragged from the OS, preventing interference with SortableJS reordering.
2025-12-28 21:13:02 -07:00
d67897aa17 Fix Media Manager critical syntax errors and enforce ES5 architecture
- Resolved persistent 'SyntaxError: Unexpected token class' by refactoring 'MediaState' and 'UI' classes in MediaManager.html to standard ES5 function constructors.

- Resolved 'SyntaxError: Unexpected identifier src' by rewriting 'createCard' to use 'document.createElement' instead of template strings for dynamic media elements.

- Consolidated script tags in MediaManager.html to prevent Apps Script parser merge issues.

- Updated docs/ARCHITECTURE.md and MEMORY.md to formally document client-side constraints (No ES6 classes, strict DOM manipulation for media).

- Note: Google Drive video animate-on-hover functionality is implemented but currently pending verification/fix.
2025-12-28 20:35:29 -07:00
c738ab3ef7 Refactor Media Manager UI and Fix Infinite Loop
- **UI Refactor**:
  - Split Media Manager header into two distinct cards: 'Product Info' and 'Upload Options'.
  - 'Product Info' now displays the Product Title and SKU.
  - Renamed upload buttons to 'Google Drive', 'Google Photos', and 'Your Computer' for clarity.
  - Added global drag-and-drop support with overlay.
  - Replaced full-screen 'Connecting' overlay with an inline spinner for better UX and log visibility.

- **Backend**:
  - Renamed getSelectedSku to getSelectedProductInfo in mediaHandlers.ts to fetch and return both SKU and Title.
  - Updated global.ts exports and mediaHandlers.test.ts to support the new signature.

- **Fixes**:
  - Resolved an infinite loop issue in loadMedia caused by incorrect SKU state handling.
2025-12-28 16:34:02 -07:00
d9d884e1fc Improve Media Manager loading state visibility
- Removed the full-screen 'Connecting' overlay in MediaManager.html

- Implemented an inline loading spinner control

- Ensure log container is visible immediately during initialization so users can track progress
2025-12-28 16:02:56 -07:00
8 changed files with 726 additions and 488 deletions

View File

@ -40,5 +40,9 @@ This project (`product_inventory`) integrates Google Sheets with Shopify. It ser
1. Sanitize with `Utilities.newBlob()`. 1. Sanitize with `Utilities.newBlob()`.
2. Fallback to **Advanced Drive Service** (`Drive.Files.create` / `v3`) if standard creation fails. 2. Fallback to **Advanced Drive Service** (`Drive.Files.create` / `v3`) if standard creation fails.
- **Video Previews**: - **Video Previews**:
- HTML5 `<video>` tags often fail with standard Drive download URLs due to auth/codec issues. - **Video Previews**:
- **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. - 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.

View File

@ -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. - Calculates checksums to avoid re-uploading duplicate images.
- Uses Shopify's "Staged Uploads" -> "Create Media" mutation flow. - 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

View File

@ -23,7 +23,7 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar" import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
import { checkRecentSales, reconcileSalesHandler } from "./salesSync" import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
import { installSalesSyncTrigger } from "./triggers" 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" import { runSystemDiagnostics } from "./verificationSuite"
// prettier-ignore // prettier-ignore
@ -52,7 +52,7 @@ import { runSystemDiagnostics } from "./verificationSuite"
;(global as any).reconcileSalesHandler = reconcileSalesHandler ;(global as any).reconcileSalesHandler = reconcileSalesHandler
;(global as any).installSalesSyncTrigger = installSalesSyncTrigger ;(global as any).installSalesSyncTrigger = installSalesSyncTrigger
;(global as any).showMediaManager = showMediaManager ;(global as any).showMediaManager = showMediaManager
;(global as any).getSelectedSku = getSelectedSku ;(global as any).getSelectedProductInfo = getSelectedProductInfo
;(global as any).getMediaForSku = getMediaForSku ;(global as any).getMediaForSku = getMediaForSku
;(global as any).saveFileToDrive = saveFileToDrive ;(global as any).saveFileToDrive = saveFileToDrive
;(global as any).saveMediaChanges = saveMediaChanges ;(global as any).saveMediaChanges = saveMediaChanges

View File

@ -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 { Config } from "./config"
import { GASDriveService } from "./services/GASDriveService" import { GASDriveService } from "./services/GASDriveService"
import { GASSpreadsheetService } from "./services/GASSpreadsheetService" import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
@ -47,7 +47,11 @@ jest.mock("./services/GASSpreadsheetService", () => {
return { return {
GASSpreadsheetService: jest.fn().mockImplementation(() => { GASSpreadsheetService: jest.fn().mockImplementation(() => {
return { 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") expect(mockUi.showModalDialog).toHaveBeenCalledWith(mockHtml, "Media Manager")
}) })
test("getSelectedSku should return sku from sheet", () => { test("getSelectedProductInfo should return sku and title from sheet", () => {
const sku = getSelectedSku() const info = getSelectedProductInfo()
expect(sku).toBe("TEST-SKU") expect(info).toEqual({ sku: "TEST-SKU", title: "Test Product Title" })
}) })
test("getPickerConfig should return config", () => { test("getPickerConfig should return config", () => {

View File

@ -15,7 +15,7 @@ export function showMediaManager() {
SpreadsheetApp.getUi().showModalDialog(html, "Media Manager"); SpreadsheetApp.getUi().showModalDialog(html, "Media Manager");
} }
export function getSelectedSku(): string | null { export function getSelectedProductInfo(): { sku: string, title: string } | null {
const ss = new GASSpreadsheetService() const ss = new GASSpreadsheetService()
const sheet = SpreadsheetApp.getActiveSheet() const sheet = SpreadsheetApp.getActiveSheet()
if (sheet.getName() !== "product_inventory") return null if (sheet.getName() !== "product_inventory") return null
@ -24,7 +24,9 @@ export function getSelectedSku(): string | null {
if (row <= 1) return null // Header if (row <= 1) return null // Header
const sku = ss.getCellValueByColumnName("product_inventory", row, "sku") 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() { export function getPickerConfig() {
@ -105,7 +107,13 @@ export function getMediaDiagnostics(sku: string) {
const shopifyId = product.shopify_id || "" 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) { export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {

View File

@ -139,6 +139,20 @@ export class MediaService {
// Find Shopify Orphans // Find Shopify Orphans
shopifyMedia.forEach(m => { shopifyMedia.forEach(m => {
if (!matchedShopifyIds.has(m.id)) { 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({ unifiedState.push({
id: m.id, // Use Shopify ID keys for orphans id: m.id, // Use Shopify ID keys for orphans
driveId: null, driveId: null,
@ -148,7 +162,9 @@ export class MediaService {
source: 'shopify_only', source: 'shopify_only',
thumbnail: m.preview?.image?.originalSrc || "", thumbnail: m.preview?.image?.originalSrc || "",
status: 'active', status: 'active',
galleryOrder: 10000 // End of list galleryOrder: 10000, // End of list
mimeType: mimeType,
contentUrl: contentUrl
}) })
} }
}) })

View File

@ -78,6 +78,17 @@ export class ShopifyMediaService implements IShopifyMediaService {
originalSrc originalSrc
} }
} }
... on Video {
sources {
url
mimeType
}
}
... on MediaImage {
image {
url
}
}
} }
} }
} }