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,30 +302,40 @@
|
||||
<body>
|
||||
<div id="main-ui" style="display:none">
|
||||
<!-- Header Card -->
|
||||
<!-- Product Info Card -->
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<h2>Media Manager</h2>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||
<div>
|
||||
<div id="current-title" style="font-weight:600; font-size:16px; margin-bottom:4px; color:var(--text);">Loading...
|
||||
</div>
|
||||
<span id="current-sku" class="sku-badge">...</span>
|
||||
</div>
|
||||
<!-- Optional: Could put metadata here or small status -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
|
||||
<div style="font-size: 32px; margin-bottom: 8px;">☁️</div>
|
||||
<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
|
||||
<!-- Upload Options Card -->
|
||||
<div class="card">
|
||||
<div class="header" style="margin-bottom: 12px;">
|
||||
<h3 style="margin:0; font-size:14px; color:var(--text);">Add Photos/Videos from...</h3>
|
||||
</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>
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||
<button onclick="controller.openPicker()" class="btn btn-secondary" style="flex: 1; font-size: 12px;">
|
||||
📂 Drive Picker
|
||||
</button>
|
||||
<button onclick="controller.startPhotoSession()" class="btn btn-secondary" style="flex: 1; font-size: 12px;">
|
||||
📸 Google Photos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Photos Session UI -->
|
||||
<div id="photos-session-ui"
|
||||
style="display:none; margin-top:12px; padding:12px; background:#f0f9ff; border-radius:8px; border:1px solid #bae6fd;">
|
||||
@ -411,6 +421,12 @@
|
||||
</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>
|
||||
/**
|
||||
* State Management
|
||||
@ -422,11 +438,12 @@
|
||||
this.initialState = []; // For diffing "isDirty"
|
||||
}
|
||||
|
||||
setSku(sku) {
|
||||
this.sku = sku;
|
||||
setSku(info) {
|
||||
this.sku = info ? info.sku : null;
|
||||
this.title = info ? info.title : "";
|
||||
this.items = [];
|
||||
this.initialState = [];
|
||||
ui.updateSku(sku);
|
||||
ui.updateSku(this.sku, this.title);
|
||||
}
|
||||
|
||||
setItems(items) {
|
||||
@ -562,8 +579,9 @@
|
||||
this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log";
|
||||
}
|
||||
|
||||
updateSku(sku) {
|
||||
document.getElementById('current-sku').innerText = sku;
|
||||
updateSku(sku, title) {
|
||||
document.getElementById('current-sku').innerText = sku || '...';
|
||||
document.getElementById('current-title').innerText = title || '';
|
||||
document.getElementById('loading-ui').style.display = 'none';
|
||||
document.getElementById('main-ui').style.display = 'block';
|
||||
}
|
||||
@ -791,23 +809,30 @@
|
||||
|
||||
checkSku() {
|
||||
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) {
|
||||
state.setSku(sku);
|
||||
state.setSku(info); // Pass whole object
|
||||
this.loadMedia();
|
||||
}
|
||||
})
|
||||
.getSelectedSku();
|
||||
.getSelectedProductInfo();
|
||||
},
|
||||
|
||||
loadMedia(preserveLogs = false) {
|
||||
const sku = document.getElementById('current-sku').innerText;
|
||||
// Ensure Loading UI is visible and Main UI is hidden until ready
|
||||
document.getElementById('loading-ui').style.display = 'block';
|
||||
document.getElementById('main-ui').style.display = 'none';
|
||||
// Resolve SKU/Title - prefer state, fallback to DOM
|
||||
let sku = state.sku;
|
||||
let title = state.title;
|
||||
|
||||
// Reset State (this calls ui.updateSku which might show main-ui, so we re-toggle below if needed)
|
||||
state.setSku(sku);
|
||||
if (!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
|
||||
document.getElementById('loading-ui').style.display = 'none';
|
||||
@ -816,6 +841,9 @@
|
||||
// Set Inline Loading State
|
||||
ui.setLoadingState(true);
|
||||
|
||||
// Reset State (this calls ui.updateSku)
|
||||
state.setSku({ sku, title });
|
||||
|
||||
if (!preserveLogs) {
|
||||
document.getElementById('status-log-container').innerHTML = '';
|
||||
}
|
||||
@ -1022,14 +1050,33 @@
|
||||
// Init
|
||||
controller.init();
|
||||
|
||||
// Drag & Drop Handlers for upload zone (Visual only)
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
// Drag & Drop Handlers (Global)
|
||||
const dropOverlay = document.getElementById('drop-overlay');
|
||||
let dragCounter = 0;
|
||||
|
||||
document.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('dragover');
|
||||
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();
|
||||
dragCounter = 0;
|
||||
dropOverlay.style.display = 'none';
|
||||
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
||||
controller.handleFiles(e.dataTransfer.files);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@ -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, 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"
|
||||
|
||||
// prettier-ignore
|
||||
@ -52,7 +52,7 @@ import { runSystemDiagnostics } from "./verificationSuite"
|
||||
;(global as any).reconcileSalesHandler = reconcileSalesHandler
|
||||
;(global as any).installSalesSyncTrigger = installSalesSyncTrigger
|
||||
;(global as any).showMediaManager = showMediaManager
|
||||
;(global as any).getSelectedSku = getSelectedSku
|
||||
;(global as any).getSelectedProductInfo = getSelectedProductInfo
|
||||
;(global as any).getMediaForSku = getMediaForSku
|
||||
;(global as any).saveFileToDrive = saveFileToDrive
|
||||
;(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 { GASDriveService } from "./services/GASDriveService"
|
||||
import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
|
||||
@ -47,7 +47,11 @@ jest.mock("./services/GASSpreadsheetService", () => {
|
||||
return {
|
||||
GASSpreadsheetService: jest.fn().mockImplementation(() => {
|
||||
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")
|
||||
})
|
||||
|
||||
test("getSelectedSku should return sku from sheet", () => {
|
||||
const sku = getSelectedSku()
|
||||
expect(sku).toBe("TEST-SKU")
|
||||
test("getSelectedProductInfo should return sku and title from sheet", () => {
|
||||
const info = getSelectedProductInfo()
|
||||
expect(info).toEqual({ sku: "TEST-SKU", title: "Test Product Title" })
|
||||
})
|
||||
|
||||
test("getPickerConfig should return config", () => {
|
||||
|
||||
@ -15,7 +15,7 @@ export function showMediaManager() {
|
||||
SpreadsheetApp.getUi().showModalDialog(html, "Media Manager");
|
||||
}
|
||||
|
||||
export function getSelectedSku(): string | null {
|
||||
export function getSelectedProductInfo(): { sku: string, title: string } | null {
|
||||
const ss = new GASSpreadsheetService()
|
||||
const sheet = SpreadsheetApp.getActiveSheet()
|
||||
if (sheet.getName() !== "product_inventory") return null
|
||||
@ -24,7 +24,9 @@ export function getSelectedSku(): string | null {
|
||||
if (row <= 1) return null // Header
|
||||
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user