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 = "!= initialSku ?>";
+ state.title = "!= initialTitle ?>";
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 || {}
+ }))
+ }
}