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.
This commit is contained in:
@ -302,29 +302,39 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="main-ui" style="display:none">
|
<div id="main-ui" style="display:none">
|
||||||
<!-- Header Card -->
|
<!-- Header Card -->
|
||||||
|
<!-- Product Info Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="header">
|
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||||
<h2>Media Manager</h2>
|
<div>
|
||||||
<span id="current-sku" class="sku-badge">...</span>
|
<div id="current-title" style="font-weight:600; font-size:16px; margin-bottom:4px; color:var(--text);">Loading...
|
||||||
</div>
|
</div>
|
||||||
|
<span id="current-sku" class="sku-badge">...</span>
|
||||||
<div class="upload-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
|
</div>
|
||||||
<div style="font-size: 32px; margin-bottom: 8px;">☁️</div>
|
<!-- Optional: Could put metadata here or small status -->
|
||||||
<div style="font-size: 14px; font-weight: 500;">Drop files or click to upload</div>
|
|
||||||
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">
|
|
||||||
Direct to Drive • JPG, PNG, MP4
|
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="file-input" multiple style="display:none" onchange="controller.handleFiles(this.files)">
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
<!-- Upload Options Card -->
|
||||||
<button onclick="controller.openPicker()" class="btn btn-secondary" style="flex: 1; font-size: 12px;">
|
<div class="card">
|
||||||
📂 Drive Picker
|
<div class="header" style="margin-bottom: 12px;">
|
||||||
</button>
|
<h3 style="margin:0; font-size:14px; color:var(--text);">Add Photos/Videos from...</h3>
|
||||||
<button onclick="controller.startPhotoSession()" class="btn btn-secondary" style="flex: 1; font-size: 12px;">
|
|
||||||
📸 Google Photos
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; width: 100%;">
|
||||||
|
<button onclick="controller.openPicker()" class="btn btn-secondary"
|
||||||
|
style="flex: 1; font-size: 13px; white-space: nowrap;">
|
||||||
|
Google Drive
|
||||||
|
</button>
|
||||||
|
<button onclick="controller.startPhotoSession()" class="btn btn-secondary"
|
||||||
|
style="flex: 1; font-size: 13px; white-space: nowrap;">
|
||||||
|
Google Photos
|
||||||
|
</button>
|
||||||
|
<button onclick="document.getElementById('file-input').click()" class="btn btn-secondary"
|
||||||
|
style="flex: 1; font-size: 13px;">
|
||||||
|
Your Computer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="file-input" multiple style="display:none" onchange="controller.handleFiles(this.files)">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Photos Session UI -->
|
<!-- Photos Session UI -->
|
||||||
<div id="photos-session-ui"
|
<div id="photos-session-ui"
|
||||||
@ -411,6 +421,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="drop-overlay"
|
||||||
|
style="position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(37, 99, 235, 0.9); z-index: 200; display: none; flex-direction: column; align-items: center; justify-content: center; color: white;">
|
||||||
|
<div style="font-size: 48px; margin-bottom: 16px;">☁️</div>
|
||||||
|
<div style="font-size: 24px; font-weight: 600;">Drop files to Upload</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* State Management
|
* State Management
|
||||||
@ -422,11 +438,12 @@
|
|||||||
this.initialState = []; // For diffing "isDirty"
|
this.initialState = []; // For diffing "isDirty"
|
||||||
}
|
}
|
||||||
|
|
||||||
setSku(sku) {
|
setSku(info) {
|
||||||
this.sku = sku;
|
this.sku = info ? info.sku : null;
|
||||||
|
this.title = info ? info.title : "";
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.initialState = [];
|
this.initialState = [];
|
||||||
ui.updateSku(sku);
|
ui.updateSku(this.sku, this.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
setItems(items) {
|
setItems(items) {
|
||||||
@ -562,8 +579,9 @@
|
|||||||
this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log";
|
this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSku(sku) {
|
updateSku(sku, title) {
|
||||||
document.getElementById('current-sku').innerText = sku;
|
document.getElementById('current-sku').innerText = sku || '...';
|
||||||
|
document.getElementById('current-title').innerText = title || '';
|
||||||
document.getElementById('loading-ui').style.display = 'none';
|
document.getElementById('loading-ui').style.display = 'none';
|
||||||
document.getElementById('main-ui').style.display = 'block';
|
document.getElementById('main-ui').style.display = 'block';
|
||||||
}
|
}
|
||||||
@ -791,23 +809,30 @@
|
|||||||
|
|
||||||
checkSku() {
|
checkSku() {
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(sku => {
|
.withSuccessHandler(info => {
|
||||||
|
// Info is now { sku, title } or null
|
||||||
|
const sku = info ? info.sku : null;
|
||||||
|
|
||||||
if (sku && sku !== state.sku) {
|
if (sku && sku !== state.sku) {
|
||||||
state.setSku(sku);
|
state.setSku(info); // Pass whole object
|
||||||
this.loadMedia();
|
this.loadMedia();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.getSelectedSku();
|
.getSelectedProductInfo();
|
||||||
},
|
},
|
||||||
|
|
||||||
loadMedia(preserveLogs = false) {
|
loadMedia(preserveLogs = false) {
|
||||||
const sku = document.getElementById('current-sku').innerText;
|
// Resolve SKU/Title - prefer state, fallback to DOM
|
||||||
// Ensure Loading UI is visible and Main UI is hidden until ready
|
let sku = state.sku;
|
||||||
document.getElementById('loading-ui').style.display = 'block';
|
let title = state.title;
|
||||||
document.getElementById('main-ui').style.display = 'none';
|
|
||||||
|
|
||||||
// Reset State (this calls ui.updateSku which might show main-ui, so we re-toggle below if needed)
|
if (!sku) {
|
||||||
state.setSku(sku);
|
const domSku = document.getElementById('current-sku').innerText;
|
||||||
|
if (domSku && domSku !== '...') sku = domSku;
|
||||||
|
|
||||||
|
const domTitle = document.getElementById('current-title').innerText;
|
||||||
|
if (domTitle && domTitle !== 'Loading...') title = domTitle;
|
||||||
|
}
|
||||||
|
|
||||||
// Show Main UI immediately so logs are visible
|
// Show Main UI immediately so logs are visible
|
||||||
document.getElementById('loading-ui').style.display = 'none';
|
document.getElementById('loading-ui').style.display = 'none';
|
||||||
@ -816,6 +841,9 @@
|
|||||||
// Set Inline Loading State
|
// Set Inline Loading State
|
||||||
ui.setLoadingState(true);
|
ui.setLoadingState(true);
|
||||||
|
|
||||||
|
// Reset State (this calls ui.updateSku)
|
||||||
|
state.setSku({ sku, title });
|
||||||
|
|
||||||
if (!preserveLogs) {
|
if (!preserveLogs) {
|
||||||
document.getElementById('status-log-container').innerHTML = '';
|
document.getElementById('status-log-container').innerHTML = '';
|
||||||
}
|
}
|
||||||
@ -1022,14 +1050,33 @@
|
|||||||
// Init
|
// Init
|
||||||
controller.init();
|
controller.init();
|
||||||
|
|
||||||
// Drag & Drop Handlers for upload zone (Visual only)
|
// Drag & Drop Handlers (Global)
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropOverlay = document.getElementById('drop-overlay');
|
||||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
let dragCounter = 0;
|
||||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); });
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
document.addEventListener('dragenter', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter++;
|
||||||
|
dropOverlay.style.display = 'flex';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter--;
|
||||||
|
if (dragCounter === 0) {
|
||||||
|
dropOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragover', (e) => { e.preventDefault(); });
|
||||||
|
|
||||||
|
document.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('dragover');
|
dragCounter = 0;
|
||||||
controller.handleFiles(e.dataTransfer.files);
|
dropOverlay.style.display = 'none';
|
||||||
|
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
||||||
|
controller.handleFiles(e.dataTransfer.files);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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", () => {
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user