diff --git a/src/MediaSidebar.html b/src/MediaManager.html
similarity index 56%
rename from src/MediaSidebar.html
rename to src/MediaManager.html
index 09609c3..63c21db 100644
--- a/src/MediaSidebar.html
+++ b/src/MediaManager.html
@@ -59,7 +59,7 @@
.upload-zone {
border: 2px dashed var(--border);
border-radius: 8px;
- padding: 24px;
+ padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
@@ -74,9 +74,9 @@
.media-grid {
display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 8px;
- margin-top: 12px;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 16px;
+ margin-top: 16px;
}
.media-item {
@@ -85,6 +85,10 @@
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border);
+ background: var(--surface);
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.media-item img {
@@ -167,7 +171,7 @@
-
+
Pick Photos
@@ -321,11 +325,13 @@
.setIncludeFolders(true)
.setSelectFolderEnabled(false);
+ const photosView = new google.picker.PhotosView();
+
const picker = new google.picker.PickerBuilder()
.addView(view)
+ .addView(photosView)
.setOAuthToken(config.token)
.setDeveloperKey(config.apiKey)
- .setOrigin(google.script.host.origin)
.setCallback(pickerCallback)
.build();
@@ -338,98 +344,99 @@
const fileId = doc.id;
const mimeType = doc.mimeType;
const name = doc.name;
- const url = doc.url; // Often the link to the file in Drive/Photos
-
- // For Photos, we might need the direct image URL, which is often in thumbnails or requires specific handling
- // doc.thumbnails contains 's75-c' style URLs. We can strip the size to get full size?
- // Actually, for Photos API items, 'url' might be the user-facing URL.
- // Let's pass the 'thumbnails' closest to original if possible, or just pass the whole doc object to backend?
- // Simpler: pass specific fields.
+ // const url = doc.url;
const imageUrl = (doc.thumbnails && doc.thumbnails.length > 0) ? doc.thumbnails[doc.thumbnails.length - 1].url : null;
google.script.run
.withSuccessHandler(() => loadMedia(currentSku))
- .importFromPicker(currentSku, fileId, mimeType, name, imageUrl);
- }
- }
+ .importFromPicker(currentSku, fileId, mimeType, name, imageUrl);
+ }
+ }
- // --- Photos Session Logic (New API) ---
+ // --- Photos Session Logic (New API) ---
- let pollingTimer = null;
+ let isProcessingPhotos = false;
- function startPhotoSession() {
- // Reset UI
- document.getElementById('photos-session-ui').style.display = 'block';
- document.getElementById('photos-session-status').innerText = "Creating session...";
- document.getElementById('photos-session-link').style.display = 'none';
+ function startPhotoSession() {
+ // Reset UI
+ document.getElementById('photos-session-ui').style.display = 'block';
+ document.getElementById('photos-session-status').innerText = "Creating session...";
+ document.getElementById('photos-session-link').style.display = 'none';
+ isProcessingPhotos = false;
- google.script.run
- .withSuccessHandler(onSessionCreated)
- .withFailureHandler(e => {
- alert('Failed to start session: ' + e.message);
- document.getElementById('photos-session-ui').style.display = 'none';
- })
- .createPhotoSession();
- }
+ google.script.run
+ .withSuccessHandler(onSessionCreated)
+ .withFailureHandler(e => {
+ alert('Failed to start session: ' + e.message);
+ document.getElementById('photos-session-ui').style.display = 'none';
+ })
+ .createPhotoSession();
+ }
- function onSessionCreated(session) {
- if (!session || !session.pickerUri) {
- alert("Failed to get picker URI");
- return;
- }
+ function onSessionCreated(session) {
+ if (!session || !session.pickerUri) {
+ alert("Failed to get picker URI");
+ return;
+ }
- const link = document.getElementById('photos-session-link');
- link.href = session.pickerUri;
- link.style.display = 'block';
- link.innerText = "Click here to pick photos ↗";
+ const link = document.getElementById('photos-session-link');
+ link.href = session.pickerUri;
+ link.style.display = 'block';
+ link.innerText = "Click here to pick photos ↗";
- document.getElementById('photos-session-status').innerText = "Waiting for you to pick photos...";
+ document.getElementById('photos-session-status').innerText = "Waiting for you to pick photos...";
- // Open automatically? Browsers block it. User must click.
- // Start polling
- if (pollingTimer) clearInterval(pollingTimer);
- pollingTimer = setInterval(() => pollSession(session.id), 2000); // Poll every 2s
- }
+ // Start recursive polling
+ pollSession(session.id);
+ }
- function pollSession(sessionId) {
- google.script.run
- .withSuccessHandler(result => {
- console.log("Poll result:", result);
- if (result.status === 'complete') {
- clearInterval(pollingTimer);
- document.getElementById('photos-session-status').innerText = "Importing photos...";
- processPickedPhotos(result.mediaItems);
- } else if (result.status === 'error') {
- document.getElementById('photos-session-status').innerText = "Error: " + result.message;
- }
- })
- .checkPhotoSession(sessionId);
- }
+ function pollSession(sessionId) {
+ if (isProcessingPhotos) return;
- function processPickedPhotos(items) {
- // Reuse importFromPicker logic logic?
- // We can call importFromPicker for each item.
- let processed = 0;
+ google.script.run
+ .withSuccessHandler(result => {
+ console.log("Poll result:", result);
+ if (result.status === 'complete') {
+ if (!isProcessingPhotos) {
+ isProcessingPhotos = true;
+ document.getElementById('photos-session-status').innerText = "Importing photos...";
+ processPickedPhotos(result.mediaItems);
+ }
+ } else if (result.status === 'error') {
+ document.getElementById('photos-session-status').innerText = "Error: " + result.message;
+ } else {
+ // Still waiting, poll again in 2s
+ setTimeout(() => pollSession(sessionId), 2000);
+ }
+ })
+ .withFailureHandler(e => {
+ console.error("Poll failed", e);
+ // Retry? Or stop? Let's retry slowly.
+ setTimeout(() => pollSession(sessionId), 5000);
+ })
+ .checkPhotoSession(sessionId);
+ }
- items.forEach(item => {
- // console.log("Processing item:", item);
- // The new Picker API returns baseUrl nested inside mediaFile
- const imageUrl = (item.mediaFile && item.mediaFile.baseUrl) ? item.mediaFile.baseUrl : item.baseUrl;
+ function processPickedPhotos(items) {
+ let processed = 0;
- google.script.run
- .withSuccessHandler(() => {
- processed++;
- if (processed === items.length) {
- document.getElementById('photos-session-status').innerText = "Done!";
- loadMedia(currentSku);
- setTimeout(() => {
- document.getElementById('photos-session-ui').style.display = 'none';
- }, 3000);
- }
- })
- .importFromPicker(currentSku, null, item.mimeType, item.filename, imageUrl);
- });
+ items.forEach(item => {
+ const imageUrl = (item.mediaFile && item.mediaFile.baseUrl) ? item.mediaFile.baseUrl : item.baseUrl;
+
+ google.script.run
+ .withSuccessHandler(() => {
+ processed++;
+ if (processed === items.length) {
+ document.getElementById('photos-session-status').innerText = "Done!";
+ loadMedia(currentSku);
+ setTimeout(() => {
+ document.getElementById('photos-session-ui').style.display = 'none';
+ }, 3000);
+ }
+ })
+ .importFromPicker(currentSku, null, item.mimeType, item.filename, imageUrl);
+ });
}
diff --git a/src/global.ts b/src/global.ts
index c42bc3e..17b45f7 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 { showMediaSidebar, getSelectedSku, getMediaForSku, saveFileToDrive, syncMediaForSku, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
+import { showMediaManager, getSelectedSku, getMediaForSku, saveFileToDrive, syncMediaForSku, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
import { runSystemDiagnostics } from "./verificationSuite"
// prettier-ignore
@@ -51,7 +51,7 @@ import { runSystemDiagnostics } from "./verificationSuite"
;(global as any).checkRecentSales = checkRecentSales
;(global as any).reconcileSalesHandler = reconcileSalesHandler
;(global as any).installSalesSyncTrigger = installSalesSyncTrigger
-;(global as any).showMediaSidebar = showMediaSidebar
+;(global as any).showMediaManager = showMediaManager
;(global as any).getSelectedSku = getSelectedSku
;(global as any).getMediaForSku = getMediaForSku
;(global as any).saveFileToDrive = saveFileToDrive
diff --git a/src/initMenu.ts b/src/initMenu.ts
index 20b5a7a..6c6e2d4 100644
--- a/src/initMenu.ts
+++ b/src/initMenu.ts
@@ -6,7 +6,7 @@ import { reinstallTriggers, installSalesSyncTrigger } from "./triggers"
import { reconcileSalesHandler } from "./salesSync"
import { toastAndLog } from "./sheetUtils"
import { showSidebar } from "./sidebar"
-import { showMediaSidebar, debugScopes } from "./mediaHandlers"
+import { showMediaManager, debugScopes } from "./mediaHandlers"
import { runSystemDiagnostics } from "./verificationSuite"
export function initMenu() {
@@ -18,7 +18,7 @@ export function initMenu() {
.addItem("Fill out product from template", fillProductFromTemplate.name)
.addItem("Match product to Shopify", matchProductToShopifyHandler.name)
.addItem("Update Shopify Product", updateShopifyProductHandler.name)
- .addItem("Media Manager", showMediaSidebar.name)
+ .addItem("Media Manager", showMediaManager.name)
)
.addSeparator()
.addSubMenu(
diff --git a/src/mediaHandlers.test.ts b/src/mediaHandlers.test.ts
index 2643b8a..9bdbe46 100644
--- a/src/mediaHandlers.test.ts
+++ b/src/mediaHandlers.test.ts
@@ -1,5 +1,5 @@
-import { importFromPicker, getMediaForSku, createPhotoSession, checkPhotoSession, debugFolderAccess, showMediaSidebar, getSelectedSku, getPickerConfig, saveFileToDrive, syncMediaForSku, debugScopes } from "./mediaHandlers"
+import { importFromPicker, getMediaForSku, createPhotoSession, checkPhotoSession, debugFolderAccess, showMediaManager, getSelectedSku, getPickerConfig, saveFileToDrive, syncMediaForSku, debugScopes } from "./mediaHandlers"
import { Config } from "./config"
import { GASDriveService } from "./services/GASDriveService"
import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
@@ -312,14 +312,25 @@ describe("mediaHandlers", () => {
})
describe("Utility Functions", () => {
- test("showMediaSidebar should render template", () => {
- const mockUi = { showSidebar: jest.fn() }
+ test("showMediaManager should render template", () => {
+ const mockUi = { showModalDialog: jest.fn() }
;(global.SpreadsheetApp.getUi as jest.Mock).mockReturnValue(mockUi)
- showMediaSidebar()
+ // Mock HTML output chain
+ const mockHtml = {
+ setTitle: jest.fn().mockReturnThis(),
+ setWidth: jest.fn().mockReturnThis(),
+ setHeight: jest.fn().mockReturnThis()
+ }
+ ;(global.HtmlService.createHtmlOutputFromFile as jest.Mock).mockReturnValue(mockHtml)
- expect(global.HtmlService.createHtmlOutputFromFile).toHaveBeenCalledWith("MediaSidebar")
- expect(mockUi.showSidebar).toHaveBeenCalled()
+ showMediaManager()
+
+ expect(global.HtmlService.createHtmlOutputFromFile).toHaveBeenCalledWith("MediaManager")
+ expect(mockHtml.setTitle).toHaveBeenCalledWith("Media Manager")
+ expect(mockHtml.setWidth).toHaveBeenCalledWith(1100)
+ expect(mockHtml.setHeight).toHaveBeenCalledWith(750)
+ expect(mockUi.showModalDialog).toHaveBeenCalledWith(mockHtml, "Media Manager")
})
test("getSelectedSku should return sku from sheet", () => {
diff --git a/src/mediaHandlers.ts b/src/mediaHandlers.ts
index 6841906..93396e0 100644
--- a/src/mediaHandlers.ts
+++ b/src/mediaHandlers.ts
@@ -7,11 +7,12 @@ import { Shop } from "./shopifyApi"
import { Config } from "./config"
import { Product } from "./Product"
-export function showMediaSidebar() {
- const html = HtmlService.createHtmlOutputFromFile("MediaSidebar")
+export function showMediaManager() {
+ const html = HtmlService.createHtmlOutputFromFile("MediaManager")
.setTitle("Media Manager")
- .setWidth(350);
- SpreadsheetApp.getUi().showSidebar(html);
+ .setWidth(1100)
+ .setHeight(750);
+ SpreadsheetApp.getUi().showModalDialog(html, "Media Manager");
}
export function getSelectedSku(): string | null {