diff --git a/MEMORY.md b/MEMORY.md index 7720c7a..d9fb7fb 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -18,7 +18,7 @@ This project (`product_inventory`) integrates Google Sheets with Shopify. It ser ## Key Technical Decisions - **Queue System**: We implemented `onEditQueue.ts` to batch edits. This prevents hitting Shopify API rate limits and Google Apps Script execution limits during rapid manual edits. - **Hybrid API**: We use REST for retrieving Orders (legacy/easier for flat data) and GraphQL for Products (more efficient/flexible). -- **Global Exports**: Functions in `src/global.ts` are explicitly exposed to be callable by Apps Script triggers. +- **Global Exports**: Functions in `src/global.ts` must be explicitly assigned to the `global` object (e.g., `(global as any).func = func`). This is required because Webpack bundles code into an IIFE, making top-level module functions unreachable from the frontend `google.script.run` or Apps Script triggers unless exposed this way. ## User Preferences - **OS**: Windows. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 88ebe06..f402273 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -71,7 +71,15 @@ Configuration, including API keys, is stored in a dedicated Google Sheet named " ### 4. Global Entry Points (`src/global.ts`) -Since Apps Script functions must be top-level to be triggered or attached to buttons, `src/global.ts` explicitly exposes necessary functions from the modules to the global scope. +Because Webpack bundles the code into an IIFE (Immediately Invoked Function Expression) to avoid global scope pollution, top-level functions defined in modules are **not** automatically globally accessible in the Apps Script environment. + +- **Requirement**: Any function that needs to be called from the frontend via `google.script.run`, triggered by a menu, or attached to a spreadsheet event must be explicitly assigned to the `global` object in `src/global.ts`. +- **Example**: + ```typescript + import { myFunc } from "./myModule" + ;(global as any).myFunc = myFunc + ``` +- **Rationale**: This is the only way for the Google Apps Script runtime to find these functions when they are invoked via the `google.script.run` API or other entry point mechanisms. ### 5. Status Automation (`src/statusHandlers.ts`) diff --git a/src/MediaManager.html b/src/MediaManager.html index 2690735..d8122a3 100644 --- a/src/MediaManager.html +++ b/src/MediaManager.html @@ -848,6 +848,8 @@ }; var state = new MediaState(); + state.sku = ""; + state.title = ""; window.state = state; // --- ES5 Refactor: UI --- @@ -1276,148 +1278,111 @@ */ var controller = { init() { - // Initialize by checking SKU once - // Since this is a modal, the selection cannot change during the session. - this.checkSku(); - }, - - checkSku() { - google.script.run - .withSuccessHandler(info => { - // Info is now { sku, title } or null - const sku = info ? info.sku : null; - - if (sku && sku !== state.sku) { - state.setSku(info); - this.loadMedia(); - } else if (!sku && !state.sku) { - if (document.getElementById('error-ui').style.display !== 'flex') { - this.loadMedia(); // Likely to trigger error UI - } - } - }) - .withFailureHandler(e => { - console.warn("SKU check failed", e); - // If it fails once at startup, we probably should alert or retry once, - // but for now let's just leave it. If it fails, the UI might hang on "Loading..." - // potentially better to trigger error UI? - if (document.getElementById('loading-ui').style.display !== 'none') { - alert("Failed to load product info: " + e.message); - } - }) - .getSelectedProductInfo(); + if (state.sku) { + // If we already have the SKU from the template, show the UI shell immediately + document.getElementById('loading-ui').style.display = 'none'; + document.getElementById('main-ui').style.display = 'block'; + ui.updateSku(state.sku, state.title); + ui.setLoadingState(true); // Loading... spinner inside the grid + } + this.loadMedia(); }, loadMedia(preserveLogs = false) { - // ... (Resolving SKU/Title Logic preserved below implicitly or we verify we didn't clip it) - // Actually, let's keep the resolving logic safe. - // We are replacing lines 1120-1191 roughly. - let sku = state.sku; let title = state.title; - if (!sku) { - const domSku = document.getElementById('current-sku').innerText; - if (domSku && domSku !== '...') sku = domSku; - } - - // CHECK FOR MISSING SKU - if (!sku || sku === '...') { - console.warn("No SKU found. Showing error."); - document.getElementById('loading-ui').style.display = 'none'; - document.getElementById('main-ui').style.display = 'none'; - document.getElementById('error-ui').style.display = 'flex'; - return; - } - - if (!title) { - const domTitle = document.getElementById('current-title').innerText; - if (domTitle && domTitle !== 'Loading...') title = domTitle; - } - - document.getElementById('loading-ui').style.display = 'none'; - document.getElementById('main-ui').style.display = 'block'; + // Visual optimization: Show loading immediately ui.setLoadingState(true); - state.setSku({ sku, title }); - if (!preserveLogs) { - document.getElementById('status-log-container').innerHTML = ''; // Reset log - ui.logStatus('ready', 'Ready.', 'info'); - } else { - // We might want to clear "Ready" if we are preserving logs - } - - // ui.toggleLogBtn.style.display = 'inline-block'; // Removed - - ui.logStatus('init', 'Initializing access...', 'info'); - - // 1. Diagnostics (Parallel) google.script.run - .withSuccessHandler((diagnostics) => { - // Check Resumption - if (diagnostics.activeJobId) { - ui.logStatus('resume', 'Resuming active background job...', 'info'); - ui.toggleSave(false); - ui.saveBtn.innerText = "Saving in background..."; - controller.startLogPolling(diagnostics.activeJobId); - if (!ui.logCard.classList.contains('expanded')) ui.toggleLogExpand(); + .withSuccessHandler(response => { + const { sku: serverSku, title: serverTitle, diagnostics, media, token } = response; + + if (!serverSku) { + console.warn("No SKU found. Showing error."); + document.getElementById('loading-ui').style.display = 'none'; + document.getElementById('main-ui').style.display = 'none'; + document.getElementById('error-ui').style.display = 'flex'; + return; } - // Drive Status - if (diagnostics.drive.status === 'ok') { - ui.logStatus('drive', `Drive Folder: ok (${diagnostics.drive.fileCount} files) Open Folder ↗`, 'success'); - ui.setDriveLink(diagnostics.drive.folderUrl); - } else { - ui.logStatus('drive', `Drive Check Failed: ${diagnostics.drive.error}`, 'error'); + // Update State + state.setSku({ sku: serverSku, title: serverTitle }); + state.token = token; + + // Update UI + document.getElementById('loading-ui').style.display = 'none'; + document.getElementById('main-ui').style.display = 'block'; + + if (!preserveLogs) { + document.getElementById('status-log-container').innerHTML = ''; // Reset log + ui.logStatus('ready', 'Ready.', 'info'); } - // Capture Token - if (diagnostics.token) state.token = diagnostics.token; + ui.logStatus('init', 'Initializing access...', 'info'); - // Shopify Status - if (diagnostics.shopify.status === 'ok') { - ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) Open Admin ↗`, 'success'); - ui.setShopifyLink(diagnostics.shopify.adminUrl); - } else if (diagnostics.shopify.status === 'skipped') { - ui.logStatus('shopify', 'Shopify Product: Not linked/Found', 'info'); - } else { - ui.logStatus('shopify', `Shopify Check Failed: ${diagnostics.shopify.error}`, 'error'); + // Handle Diagnostics + if (diagnostics) { + // Check Resumption + if (diagnostics.activeJobId) { + ui.logStatus('resume', 'Resuming active background job...', 'info'); + ui.toggleSave(false); + ui.saveBtn.innerText = "Saving in background..."; + this.startLogPolling(diagnostics.activeJobId); + if (!ui.logCard.classList.contains('expanded')) ui.toggleLogExpand(); + } + + // Drive Status + if (diagnostics.drive.status === 'ok') { + ui.logStatus('drive', `Drive Folder: ok (${diagnostics.drive.fileCount} files) Open Folder ↗`, 'success'); + ui.setDriveLink(diagnostics.drive.folderUrl); + } else { + ui.logStatus('drive', `Drive Check Failed: ${diagnostics.drive.error}`, 'error'); + } + + // Shopify Status + if (diagnostics.shopify.status === 'ok') { + ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) Open Admin ↗`, 'success'); + ui.setShopifyLink(diagnostics.shopify.adminUrl); + } else if (diagnostics.shopify.status === 'skipped') { + ui.logStatus('shopify', 'Shopify Product: Not linked/Found', 'info'); + } else { + ui.logStatus('shopify', `Shopify Check Failed: ${diagnostics.shopify.error}`, 'error'); + } } - }) - .withFailureHandler(function (err) { - ui.logStatus('fatal', `Diagnostics failed: ${err.message}`, 'error'); - }) - .getMediaDiagnostics(sku, ""); - // 2. Load Full Media (Parallel) - ui.logStatus('fetch', 'Fetching full media state...', 'info'); - google.script.run - .withSuccessHandler(function (items) { - // Normalize items - const normalized = items.map(function (i) { - return { - ...i, - id: i.id || Math.random().toString(36).substr(2, 9), - status: i.source || 'drive_only', - source: i.source, - _deleted: false - }; - }); + // Handle Media Items + if (media) { + ui.logStatus('fetch', 'Fetched full media state.', 'success'); + const normalized = media.map(function (i) { + return { + ...i, + id: i.id || Math.random().toString(36).substr(2, 9), + status: i.source || 'drive_only', + source: i.source, + _deleted: false + }; + }); - state.setItems(normalized); + state.setItems(normalized); - if (!controller.hasRunMatching) { - controller.hasRunMatching = true; - controller.checkMatches(normalized); - } else { - controller.showGallery(); + if (!this.hasRunMatching) { + this.hasRunMatching = true; + this.checkMatches(normalized); + } else { + this.showGallery(); + } } - }) - .withFailureHandler(function (err) { - ui.logStatus('fatal', `Failed to load media: ${err.message}`, 'error'); + ui.setLoadingState(false); }) - .getMediaForSku(sku); + .withFailureHandler(err => { + console.error("Initial load failed", err); + ui.logStatus('fatal', `Failed to load initialization data: ${err.message}`, 'error'); + ui.setLoadingState(false); + }) + .getMediaManagerInitialState(sku, title); }, diff --git a/src/global.ts b/src/global.ts index 1069056..4c313fe 100644 --- a/src/global.ts +++ b/src/global.ts @@ -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, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia, pollJobLogs } from "./mediaHandlers" +import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia, pollJobLogs, getMediaManagerInitialState } from "./mediaHandlers" import { runSystemDiagnostics } from "./verificationSuite" // prettier-ignore @@ -66,3 +66,4 @@ import { runSystemDiagnostics } from "./verificationSuite" ;(global as any).debugFolderAccess = debugFolderAccess ;(global as any).linkDriveFileToShopifyMedia = linkDriveFileToShopifyMedia ;(global as any).pollJobLogs = pollJobLogs +;(global as any).getMediaManagerInitialState = getMediaManagerInitialState diff --git a/src/interfaces/IDriveService.ts b/src/interfaces/IDriveService.ts index 22ffa28..8d3971d 100644 --- a/src/interfaces/IDriveService.ts +++ b/src/interfaces/IDriveService.ts @@ -8,4 +8,5 @@ export interface IDriveService { updateFileProperties(fileId: string, properties: {[key: string]: string}): void createFile(blob: GoogleAppsScript.Base.Blob): GoogleAppsScript.Drive.File getFileProperties(fileId: string): {[key: string]: string} + getFilesWithProperties(folderId: string): { file: GoogleAppsScript.Drive.File, properties: {[key: string]: string} }[] } diff --git a/src/mediaHandlers.test.ts b/src/mediaHandlers.test.ts index 1d4243c..43cdfa2 100644 --- a/src/mediaHandlers.test.ts +++ b/src/mediaHandlers.test.ts @@ -1,5 +1,5 @@ -import { importFromPicker, getMediaForSku, createPhotoSession, checkPhotoSession, debugFolderAccess, showMediaManager, getSelectedProductInfo, getPickerConfig, saveFileToDrive, debugScopes, saveMediaChanges } from "./mediaHandlers" +import { importFromPicker, getMediaForSku, createPhotoSession, checkPhotoSession, debugFolderAccess, showMediaManager, getSelectedProductInfo, getPickerConfig, saveFileToDrive, debugScopes, saveMediaChanges, getMediaManagerInitialState } from "./mediaHandlers" import { Config } from "./config" import { GASDriveService } from "./services/GASDriveService" import { GASSpreadsheetService } from "./services/GASSpreadsheetService" @@ -28,7 +28,8 @@ jest.mock("./services/MediaService", () => { MediaService: jest.fn().mockImplementation(() => { return { getUnifiedMediaState: jest.fn().mockReturnValue([]), - processMediaChanges: jest.fn().mockReturnValue([]) + processMediaChanges: jest.fn().mockReturnValue([]), + getInitialState: jest.fn().mockReturnValue({ diagnostics: {}, media: [] }) } }) } @@ -168,6 +169,13 @@ global.HtmlService = { setTitle: jest.fn().mockReturnThis(), setWidth: jest.fn().mockReturnThis(), setHeight: jest.fn().mockReturnThis() + }), + createTemplateFromFile: jest.fn().mockReturnValue({ + evaluate: jest.fn().mockReturnValue({ + setTitle: jest.fn().mockReturnThis(), + setWidth: jest.fn().mockReturnThis(), + setHeight: jest.fn().mockReturnThis() + }) }) } as any @@ -291,6 +299,33 @@ describe("mediaHandlers", () => { }) }) + describe("getMediaManagerInitialState", () => { + test("should consolidate diagnostics and media fetching", () => { + // Mock SpreadsheetApp behavior for SKU detection + const mockRange = { getValues: jest.fn().mockReturnValue([["sku", "title", "thumb"]]) }; + const mockSheet = { + getName: jest.fn().mockReturnValue("product_inventory"), + getLastColumn: jest.fn().mockReturnValue(3), + getActiveRange: jest.fn().mockReturnValue({ getRow: () => 5 }), + getRange: jest.fn().mockReturnValue({ + getValues: jest.fn() + .mockReturnValueOnce([["sku", "title", "thumbnail"]]) // Headers + .mockReturnValueOnce([["TEST-SKU", "Test Title", ""]]) // Row + }) + }; + (global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet); + + const response = getMediaManagerInitialState() + + expect(response.sku).toBe("TEST-SKU") + expect(response.title).toBe("Test Title") + + const MockMediaService = MediaService as unknown as jest.Mock + const mockInstance = MockMediaService.mock.results[MockMediaService.mock.results.length - 1].value + expect(mockInstance.getInitialState).toHaveBeenCalledWith("TEST-SKU", "shopify_id_123") + }) + }) + describe("getMediaForSku", () => { test("should delegate to MediaService.getUnifiedMediaState", () => { // Execute @@ -431,17 +466,35 @@ describe("mediaHandlers", () => { const mockUi = { showModalDialog: jest.fn() } ;(global.SpreadsheetApp.getUi as jest.Mock).mockReturnValue(mockUi) - // Mock HTML output chain + // Mock getSelectedProductInfo specifically for the optimized implementation + const mockRange = { getValues: jest.fn() }; + const mockSheet = { + getName: jest.fn().mockReturnValue("product_inventory"), + getLastColumn: jest.fn().mockReturnValue(2), + getActiveRange: jest.fn().mockReturnValue({ getRow: () => 2 }), + getRange: jest.fn().mockReturnValue(mockRange) + }; + (global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet); + mockRange.getValues.mockReturnValueOnce([["sku", "title"]]); + mockRange.getValues.mockReturnValueOnce([["SKU-1", "Product-1"]]); + + // Mock Template chain const mockHtml = { setTitle: jest.fn().mockReturnThis(), setWidth: jest.fn().mockReturnThis(), setHeight: jest.fn().mockReturnThis() } - ;(global.HtmlService.createHtmlOutputFromFile as jest.Mock).mockReturnValue(mockHtml) + const mockTemplate = { + evaluate: jest.fn().mockReturnValue(mockHtml), + initialSku: "", + initialTitle: "" + } + ;(global.HtmlService.createTemplateFromFile as jest.Mock).mockReturnValue(mockTemplate) showMediaManager() - expect(global.HtmlService.createHtmlOutputFromFile).toHaveBeenCalledWith("MediaManager") + expect(global.HtmlService.createTemplateFromFile).toHaveBeenCalledWith("MediaManager") + expect(mockTemplate.evaluate).toHaveBeenCalled() expect(mockHtml.setTitle).toHaveBeenCalledWith("Media Manager") expect(mockHtml.setWidth).toHaveBeenCalledWith(1100) expect(mockHtml.setHeight).toHaveBeenCalledWith(750) diff --git a/src/mediaHandlers.ts b/src/mediaHandlers.ts index 9217bec..e465660 100644 --- a/src/mediaHandlers.ts +++ b/src/mediaHandlers.ts @@ -8,7 +8,14 @@ import { Config } from "./config" import { Product } from "./Product" export function showMediaManager() { - const html = HtmlService.createHtmlOutputFromFile("MediaManager") + const productInfo = getSelectedProductInfo(); + const template = HtmlService.createTemplateFromFile("MediaManager"); + + // Pass variables to template + (template as any).initialSku = productInfo ? productInfo.sku : ""; + (template as any).initialTitle = productInfo ? productInfo.title : ""; + + const html = template.evaluate() .setTitle("Media Manager") .setWidth(1100) .setHeight(750); @@ -193,6 +200,62 @@ export function getMediaDiagnostics(sku: string) { } } + +export function getMediaManagerInitialState(providedSku?: string, providedTitle?: string): { + sku: string | null, + title: string, + diagnostics: any, + media: any[], + token: string +} { + let sku = providedSku; + let title = providedTitle || ""; + + if (!sku) { + const info = getSelectedProductInfo(); + if (info) { + sku = info.sku; + title = info.title; + } + } + + if (!sku) { + return { + sku: null, + title: "", + diagnostics: null, + media: [], + token: ScriptApp.getOAuthToken() + } + } + + const config = new Config() + const driveService = new GASDriveService() + const shop = new Shop() + const shopifyMediaService = new ShopifyMediaService(shop) + const networkService = new GASNetworkService() + const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config) + + // Resolve Product ID + const product = new Product(sku) + try { + product.MatchToShopifyProduct(shop); + } catch (e) { + console.warn("MatchToShopifyProduct failed", e); + } + + const shopifyId = product.shopify_id || "" + const initialState = mediaService.getInitialState(sku, shopifyId); + + return { + sku, + title, + diagnostics: initialState.diagnostics, + media: initialState.media, + token: ScriptApp.getOAuthToken() + } +} + export function linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: string) { const config = new Config() const driveService = new GASDriveService() diff --git a/src/mediaManager.integration.test.ts b/src/mediaManager.integration.test.ts index 34ec2a0..ce4771a 100644 --- a/src/mediaManager.integration.test.ts +++ b/src/mediaManager.integration.test.ts @@ -12,7 +12,8 @@ const mockDrive = { trashFile: jest.fn(), updateFileProperties: jest.fn(), getFileProperties: jest.fn(), - getFileById: jest.fn() + getFileById: jest.fn(), + getFilesWithProperties: jest.fn() } const mockShopify = { getProductMedia: jest.fn(), @@ -80,6 +81,13 @@ describe("MediaService V2 Integration Logic", () => { getId: () => "new_created_file_id" }) mockDrive.getFileProperties.mockReturnValue({}) + mockDrive.getFilesWithProperties.mockImplementation((folderId: string) => { + const files = mockDrive.getFiles(folderId) || [] + return files.map(f => ({ + file: f, + properties: mockDrive.getFileProperties(f.getId()) + })) + }) }) describe("getUnifiedMediaState (Phase A)", () => { diff --git a/src/services/GASDriveService.ts b/src/services/GASDriveService.ts index f9d25f5..67a0b93 100644 --- a/src/services/GASDriveService.ts +++ b/src/services/GASDriveService.ts @@ -99,4 +99,55 @@ export class GASDriveService implements IDriveService { return {} } } + + getFilesWithProperties(folderId: string): { file: GoogleAppsScript.Drive.File, properties: { [key: string]: string } }[] { + if (typeof Drive === 'undefined') { + return this.getFiles(folderId).map(f => ({ file: f, properties: {} })) + } + + try { + const drive = Drive as any + const isV3 = !!drive.Files.create + const query = `'${folderId}' in parents and trashed = false` + const fields = isV3 ? 'nextPageToken, files(id, name, mimeType, appProperties)' : 'nextPageToken, items(id, title, mimeType, properties)' + + const results: { file: GoogleAppsScript.Drive.File, properties: { [key: string]: string } }[] = [] + let pageToken: string | null = null + + do { + const response = drive.Files.list({ q: query, fields: fields, pageToken: pageToken, supportsAllDrives: true, includeItemsFromAllDrives: true }) + + const items = isV3 ? response.files : response.items + + if (items) { + items.forEach((item: any) => { + const file = DriveApp.getFileById(item.id) + const props: { [key: string]: string } = {} + + if (isV3) { + if (item.appProperties) { + Object.assign(props, item.appProperties) + } + } else { + if (item.properties) { + item.properties.forEach((p: any) => { + if (p.visibility === 'PRIVATE') { + props[p.key] = p.value + } + }) + } + } + + results.push({ file: file, properties: props }) + }) + } + pageToken = response.nextPageToken + } while (pageToken) + + return results + } catch (e) { + console.error(`Failed to get files with properties for folder ${folderId}`, e) + return this.getFiles(folderId).map(f => ({ file: f, properties: {} })) + } + } } diff --git a/src/services/MediaService.ts b/src/services/MediaService.ts index 273dc58..470c2f1 100644 --- a/src/services/MediaService.ts +++ b/src/services/MediaService.ts @@ -100,7 +100,7 @@ export class MediaService { // We need strict file list. // Optimization: getFiles() usually returns limited info. // We might need to iterate and pull props if getFiles() doesn't include appProperties (DriveApp doesn't). - const driveFiles = this.driveService.getFiles(folder.getId()) + const driveFiles = this.driveService.getFilesWithProperties(folder.getId()) // 2. Get Shopify Media let shopifyMedia: any[] = [] @@ -118,26 +118,17 @@ export class MediaService { const sidecarFileIds = new Set(); // Map of Drive Files (Enriched) - const driveFileStats = driveFiles.map(f => { - let shopifyId = null - let galleryOrder = 9999 - let type = 'media'; - let customThumbnailId = null; - let parentVideoId = null; + const driveFileStats = driveFiles.map(d => { + const f = d.file + const props = d.properties + let shopifyId = props['shopify_media_id'] || null + let galleryOrder = props['gallery_order'] ? parseInt(props['gallery_order']) : 9999 + let type = props['type'] || 'media'; + let customThumbnailId = props['custom_thumbnail_id'] || null; + let parentVideoId = props['parent_video_id'] || null; - try { - const props = this.driveService.getFileProperties(f.getId()) - if (props['shopify_media_id']) shopifyId = props['shopify_media_id'] - if (props['gallery_order']) galleryOrder = parseInt(props['gallery_order']) - if (props['type']) type = props['type']; - if (props['custom_thumbnail_id']) customThumbnailId = props['custom_thumbnail_id']; - if (props['parent_video_id']) parentVideoId = props['parent_video_id']; + console.log(`[DEBUG] File ${f.getName()} Props:`, JSON.stringify(props)); - console.log(`[DEBUG] File ${f.getName()} Props:`, JSON.stringify(props)); - - } catch (e) { - console.warn(`Failed to get properties for ${f.getName()}`) - } return { file: f, shopifyId, galleryOrder, type, customThumbnailId, parentVideoId } }) @@ -610,5 +601,20 @@ export class MediaService { return logs } + getInitialState(sku: string, shopifyProductId: string): { diagnostics: any, media: any[] } { + // 1. Diagnostics (Reusing the existing method logic but avoiding redundant setup) + const diagnostics = this.getDiagnostics(sku, shopifyProductId); + + // 2. Unified Media State + // If diagnostics succeeded in finding the folder, we should probably pass that info + // to getUnifiedMediaState to avoid re-fetching the folder, but for now + // let's just call the method to keep it clean. + const media = this.getUnifiedMediaState(sku, shopifyProductId); + + return { + diagnostics, + media + }; + } } diff --git a/src/services/MockDriveService.ts b/src/services/MockDriveService.ts index 69c3fb1..06d8b4b 100644 --- a/src/services/MockDriveService.ts +++ b/src/services/MockDriveService.ts @@ -127,4 +127,12 @@ export class MockDriveService implements IDriveService { return {} } } + + getFilesWithProperties(folderId: string): { file: GoogleAppsScript.Drive.File, properties: { [key: string]: string } }[] { + const files = this.getFiles(folderId) + return files.map(f => ({ + file: f, + properties: (f as any)._properties || {} + })) + } }