feat(media): Optimize Media Manager loading performance

Significant performance improvements to the 'Loading media...' phase:
- Reduced client-server round trips by consolidating the initial handshake (diagnostics + media fetch) into a single backend call: getMediaManagerInitialState.
- Implemented batched Google Drive metadata retrieval in GASDriveService using the Advanced Drive API, eliminating per-file property fetching calls.
- Switched to HtmlService templates in showMediaManager to pass initial SKU/Title data directly, enabling the UI shell to appear instantly upon opening.
- Updated documentation (ARCHITECTURE.md, MEMORY.md) to clarify Webpack global assignment requirements for GAS functions.
- Verified with comprehensive updates to unit and integration tests.
This commit is contained in:
Ben Miller
2025-12-31 09:46:56 -07:00
parent fc25e877f1
commit e39bc862cc
11 changed files with 314 additions and 150 deletions

View File

@ -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: {} }))
}
}
}

View File

@ -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<string>();
// 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
};
}
}

View File

@ -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 || {}
}))
}
}