Compare commits
10 Commits
thumbnails
...
d34f9a1417
| Author | SHA1 | Date | |
|---|---|---|---|
| d34f9a1417 | |||
| 3abc57f45a | |||
| dc33390650 | |||
| f25fb359e8 | |||
| 64ab548593 | |||
| 772957058d | |||
| 16dec5e888 | |||
| ec6602cbde | |||
| f1ab3b7b84 | |||
| ebc1a39ce3 |
57
.eslintrc.js
Normal file
57
.eslintrc.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"html",
|
||||||
|
],
|
||||||
|
globals: {
|
||||||
|
"google": "readonly",
|
||||||
|
"Logger": "readonly",
|
||||||
|
"item": "writable",
|
||||||
|
"Utilities": "readonly",
|
||||||
|
"state": "writable",
|
||||||
|
"ui": "writable",
|
||||||
|
"controller": "writable",
|
||||||
|
"gapi": "readonly",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off", // Too noisy for existing codebase
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"no-var": "off",
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-redeclare": "off",
|
||||||
|
"no-empty": "warn",
|
||||||
|
"@typescript-eslint/ban-types": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"no-extra-semi": "off",
|
||||||
|
"no-array-constructor": "off",
|
||||||
|
"@typescript-eslint/no-array-constructor": "off",
|
||||||
|
"@typescript-eslint/no-this-alias": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"no-prototype-builtins": "off"
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.html"],
|
||||||
|
parser: "espree", // Use default parser for HTML scripts if TS parser fails, or just rely on plugin handling
|
||||||
|
// Actually plugin-html handles it. But we usually need to specify not to use TS rules that require type info if we don't have full project info for snippets.
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@ desktop.ini
|
|||||||
.continue/**
|
.continue/**
|
||||||
.clasp.json
|
.clasp.json
|
||||||
coverage/
|
coverage/
|
||||||
|
test_*.txt
|
||||||
|
.agent/
|
||||||
|
|||||||
@ -46,3 +46,6 @@ This project (`product_inventory`) integrates Google Sheets with Shopify. It ser
|
|||||||
- **Client-Side Syntax**:
|
- **Client-Side Syntax**:
|
||||||
- **ES5 ONLY**: Do not use `class` in client-side HTML files. The Apps Script sanitizer often fails to parse them. Use `function` constructors.
|
- **ES5 ONLY**: Do not use `class` in client-side HTML files. The Apps Script sanitizer often fails to parse them. Use `function` constructors.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
- **Test Output**: When running tests, use `npm run test:log` to capture full output to `test_output.txt`. This avoids terminal truncation and allows agents to read the full results without manual redirection.
|
||||||
|
|
||||||
|
|||||||
1298
package-lock.json
generated
1298
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,15 +6,21 @@
|
|||||||
"global.ts"
|
"global.ts"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "npm run lint && webpack --mode production",
|
||||||
|
"lint": "eslint \"src/**/*.{ts,js,html}\"",
|
||||||
"deploy": "clasp push",
|
"deploy": "clasp push",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:log": "jest > test_output.txt 2>&1",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/google-apps-script": "^1.0.85",
|
"@types/google-apps-script": "^1.0.85",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"copy-webpack-plugin": "^13.0.1",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-plugin-html": "^8.1.3",
|
||||||
"gas-webpack-plugin": "^2.6.0",
|
"gas-webpack-plugin": "^2.6.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|||||||
@ -164,25 +164,24 @@
|
|||||||
/* Processing State */
|
/* Processing State */
|
||||||
.media-item.processing-card {
|
.media-item.processing-card {
|
||||||
background-color: #334155 !important;
|
background-color: #334155 !important;
|
||||||
position: relative; /* Ensure absolute children are contained */
|
position: relative;
|
||||||
/* Removed flex centering to let image stretch */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-item.processing-card .media-content {
|
.media-item.processing-card .media-content {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
opacity: 0.8; /* Lighter overlay (was 0.4) */
|
opacity: 0.8;
|
||||||
filter: grayscale(30%); /* Less grey (was 80%) */
|
filter: grayscale(30%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain; /* Ensure it fills */
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.processing-icon {
|
.processing-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 6px;
|
bottom: 6px;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
font-size: 20px; /* Smaller */
|
font-size: 20px;
|
||||||
z-index: 20; /* Above badges */
|
z-index: 20;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -380,6 +379,74 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Log Card Styles */
|
||||||
|
.log-card {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
padding: 12px;
|
||||||
|
/* ~1 line */
|
||||||
|
max-height: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-card.expanded .log-content {
|
||||||
|
/* ~20 lines */
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
line-height: 1.4;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar for log */
|
||||||
|
.log-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -399,42 +466,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- 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;">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
|
|
||||||
<span style="font-weight:600; font-size:12px; color:#0369a1;">Photo Picker Session</span>
|
|
||||||
<button onclick="ui.closePhotoSession()"
|
|
||||||
style="background:none; border:none; color:#0369a1; cursor:pointer; font-size:16px;">×</button>
|
|
||||||
</div>
|
|
||||||
<a id="photos-session-link" href="#" target="_blank" class="btn"
|
|
||||||
style="background:#0ea5e9; text-decoration:none; margin-bottom:8px;">
|
|
||||||
Open Google Photos ↗
|
|
||||||
</a>
|
|
||||||
<div id="photos-session-status" style="font-size:11px; color:#64748b; text-align:center;">Initializing...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -454,17 +486,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Log -->
|
|
||||||
<div id="status-log-container"
|
|
||||||
style="padding:16px; background:#f8fafc; border-bottom:1px solid var(--border); font-family:monospace; font-size:12px; line-height:1.6; display:none;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Processing Warning Banner -->
|
<!-- Processing Warning Banner -->
|
||||||
<div id="processing-banner"
|
<div id="processing-banner"
|
||||||
style="display:none; background-color:#fffbeb; color:#92400e; padding:12px; border-radius:8px; margin: 0 16px 12px 16px; font-size:13px; border:1px solid #fcd34d; align-items:flex-start; gap:8px;">
|
style="display:none; background-color:#fffbeb; color:#92400e; padding:12px; border-radius:8px; margin: 0 16px 12px 16px; font-size:13px; border:1px solid #fcd34d; align-items:flex-start; gap:8px;">
|
||||||
<span style="font-size:16px; line-height:1;">⏳</span>
|
<span style="font-size:16px; line-height:1;">⏳</span>
|
||||||
<div>
|
<div>
|
||||||
Some videos are still being transcoded by Drive. The video preview might not work yet, but they can still be saved,
|
Some videos are still being processed. The video preview might not work yet, but they can still be saved,
|
||||||
reordered, or deleted.
|
reordered, or deleted.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -482,6 +509,55 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="upload-section" style="display:none;">
|
||||||
|
<!-- 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 id="btn-upload-drive" onclick="controller.openPicker()" class="btn btn-secondary"
|
||||||
|
style="flex: 1; font-size: 13px; white-space: nowrap;">
|
||||||
|
Google Drive
|
||||||
|
</button>
|
||||||
|
<button id="btn-upload-photos" onclick="controller.startPhotoSession()" class="btn btn-secondary"
|
||||||
|
style="flex: 1; font-size: 13px; white-space: nowrap;">
|
||||||
|
Google Photos
|
||||||
|
</button>
|
||||||
|
<button id="btn-upload-computer" 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 -->
|
||||||
|
<div id="photos-session-ui"
|
||||||
|
style="display:none; margin-top:12px; padding:12px; background:#f0f9ff; border-radius:8px; border:1px solid #bae6fd;">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
|
||||||
|
<span style="font-weight:600; font-size:12px; color:#0369a1;">Photo Picker Session</span>
|
||||||
|
<button onclick="ui.closePhotoSession()"
|
||||||
|
style="background:none; border:none; color:#0369a1; cursor:pointer; font-size:16px;">×</button>
|
||||||
|
</div>
|
||||||
|
<a id="photos-session-link" href="#" target="_blank" class="btn"
|
||||||
|
style="background:#0ea5e9; text-decoration:none; margin-bottom:8px;">
|
||||||
|
Open Google Photos ↗
|
||||||
|
</a>
|
||||||
|
<div id="photos-session-status" style="font-size:11px; color:#64748b; text-align:center;">Initializing...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Permanent Log Card -->
|
||||||
|
<div id="log-card" class="log-card">
|
||||||
|
<div class="log-header" onclick="ui.toggleLogExpand()">
|
||||||
|
<span style="font-weight:600;">Activity Log</span>
|
||||||
|
<span id="log-toggle-icon">▲</span>
|
||||||
|
</div>
|
||||||
|
<div id="status-log-container" class="log-content">
|
||||||
|
<div class="log-entry" style="color: #94a3b8;">Ready.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Screen -->
|
<!-- Loading Screen -->
|
||||||
@ -708,9 +784,10 @@
|
|||||||
function UI() {
|
function UI() {
|
||||||
this.grid = document.getElementById('media-grid');
|
this.grid = document.getElementById('media-grid');
|
||||||
this.saveBtn = document.getElementById('save-btn');
|
this.saveBtn = document.getElementById('save-btn');
|
||||||
this.toggleLogBtn = document.getElementById('toggle-log-btn');
|
// this.toggleLogBtn = document.getElementById('toggle-log-btn'); // Removed
|
||||||
this.logContainer = document.getElementById('status-log-container');
|
this.logContainer = document.getElementById('status-log-container');
|
||||||
this.linksContainer = document.getElementById('quick-links');
|
this.linksContainer = document.getElementById('quick-links');
|
||||||
|
this.logCard = document.getElementById('log-card');
|
||||||
this.sortable = null;
|
this.sortable = null;
|
||||||
this.driveUrl = null;
|
this.driveUrl = null;
|
||||||
this.shopifyUrl = null;
|
this.shopifyUrl = null;
|
||||||
@ -725,10 +802,10 @@
|
|||||||
if (this.shopifyUrl) this.linksContainer.innerHTML += '<a href="' + this.shopifyUrl + '" target="_blank" style="color:var(--primary); text-decoration:none; margin-left:8px;">Shopify ↗</a>';
|
if (this.shopifyUrl) this.linksContainer.innerHTML += '<a href="' + this.shopifyUrl + '" target="_blank" style="color:var(--primary); text-decoration:none; margin-left:8px;">Shopify ↗</a>';
|
||||||
};
|
};
|
||||||
|
|
||||||
UI.prototype.toggleLog = function (forceState) {
|
UI.prototype.toggleLogExpand = function () {
|
||||||
var isVisible = typeof forceState === 'boolean' ? !forceState : this.logContainer.style.display !== 'none';
|
this.logCard.classList.toggle('expanded');
|
||||||
this.logContainer.style.display = isVisible ? 'none' : 'block';
|
var icon = this.logCard.querySelector('#log-toggle-icon');
|
||||||
this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log";
|
icon.innerText = this.logCard.classList.contains('expanded') ? '▼' : '▲';
|
||||||
};
|
};
|
||||||
|
|
||||||
UI.prototype.updateSku = function (sku, title) {
|
UI.prototype.updateSku = function (sku, title) {
|
||||||
@ -832,11 +909,21 @@
|
|||||||
UI.prototype.logStatus = function (step, message, type) {
|
UI.prototype.logStatus = function (step, message, type) {
|
||||||
if (!type) type = 'info';
|
if (!type) type = 'info';
|
||||||
var container = this.logContainer;
|
var container = this.logContainer;
|
||||||
var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : '⏳';
|
|
||||||
|
// Auto-clear "Ready"
|
||||||
|
if (container.children.length === 1 && container.children[0].innerText === "Ready.") {
|
||||||
|
container.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.innerHTML = '<span style="margin-right:8px;">' + icon + '</span> ' + message;
|
el.className = 'log-entry';
|
||||||
if (type === 'error') el.style.color = 'var(--error)';
|
el.innerHTML = '<span style="margin-right:8px; opacity:0.7;">' + icon + '</span> ' + message;
|
||||||
|
if (type === 'error') el.style.color = 'var(--danger)';
|
||||||
|
if (type === 'success') el.style.color = 'var(--success)';
|
||||||
|
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
|
container.scrollTop = container.scrollHeight; // Auto-scroll
|
||||||
};
|
};
|
||||||
|
|
||||||
UI.prototype.createCard = function (item, index) {
|
UI.prototype.createCard = function (item, index) {
|
||||||
@ -1047,10 +1134,9 @@
|
|||||||
const sku = info ? info.sku : null;
|
const sku = info ? info.sku : null;
|
||||||
|
|
||||||
if (sku && sku !== state.sku) {
|
if (sku && sku !== state.sku) {
|
||||||
state.setSku(info); // Pass whole object
|
state.setSku(info);
|
||||||
this.loadMedia();
|
this.loadMedia();
|
||||||
} else if (!sku && !state.sku) {
|
} else if (!sku && !state.sku) {
|
||||||
// If we don't have a SKU and haven't shown error yet
|
|
||||||
if (document.getElementById('error-ui').style.display !== 'flex') {
|
if (document.getElementById('error-ui').style.display !== 'flex') {
|
||||||
this.loadMedia();
|
this.loadMedia();
|
||||||
}
|
}
|
||||||
@ -1060,7 +1146,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadMedia(preserveLogs = false) {
|
loadMedia(preserveLogs = false) {
|
||||||
// Resolve SKU/Title - prefer state, fallback to DOM
|
// ... (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 sku = state.sku;
|
||||||
let title = state.title;
|
let title = state.title;
|
||||||
|
|
||||||
@ -1083,28 +1172,34 @@
|
|||||||
if (domTitle && domTitle !== 'Loading...') title = domTitle;
|
if (domTitle && domTitle !== 'Loading...') title = domTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show Main UI immediately so logs are visible
|
|
||||||
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';
|
||||||
|
|
||||||
// Set Inline Loading State
|
|
||||||
ui.setLoadingState(true);
|
ui.setLoadingState(true);
|
||||||
|
|
||||||
// Reset State (this calls ui.updateSku)
|
|
||||||
state.setSku({ sku, title });
|
state.setSku({ sku, title });
|
||||||
|
|
||||||
if (!preserveLogs) {
|
if (!preserveLogs) {
|
||||||
document.getElementById('status-log-container').innerHTML = '';
|
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';
|
|
||||||
ui.toggleLog(true); // Force Show Log to see progress
|
|
||||||
|
|
||||||
// 1. Run Diagnostics
|
// ui.toggleLogBtn.style.display = 'inline-block'; // Removed
|
||||||
// 1. Run Diagnostics
|
|
||||||
ui.logStatus('init', 'Initializing access...', 'info');
|
ui.logStatus('init', 'Initializing access...', 'info');
|
||||||
|
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(function (diagnostics) {
|
.withSuccessHandler((diagnostics) => { // Use arrow
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
// Drive Status
|
// Drive Status
|
||||||
if (diagnostics.drive.status === 'ok') {
|
if (diagnostics.drive.status === 'ok') {
|
||||||
ui.logStatus('drive', `Drive Folder: ok (${diagnostics.drive.fileCount} files) <a href="${diagnostics.drive.folderUrl}" target="_blank" style="margin-left:8px;">Open Folder ↗</a>`, 'success');
|
ui.logStatus('drive', `Drive Folder: ok (${diagnostics.drive.fileCount} files) <a href="${diagnostics.drive.folderUrl}" target="_blank" style="margin-left:8px;">Open Folder ↗</a>`, 'success');
|
||||||
@ -1116,6 +1211,7 @@
|
|||||||
// Capture Token
|
// Capture Token
|
||||||
if (diagnostics.token) state.token = diagnostics.token;
|
if (diagnostics.token) state.token = diagnostics.token;
|
||||||
|
|
||||||
|
|
||||||
// Shopify Status
|
// Shopify Status
|
||||||
if (diagnostics.shopify.status === 'ok') {
|
if (diagnostics.shopify.status === 'ok') {
|
||||||
ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success');
|
ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success');
|
||||||
@ -1166,37 +1262,81 @@
|
|||||||
ui.toggleSave(false);
|
ui.toggleSave(false);
|
||||||
ui.saveBtn.innerText = "Saving...";
|
ui.saveBtn.innerText = "Saving...";
|
||||||
|
|
||||||
ui.saveBtn.innerText = "Saving...";
|
// Generate Job ID
|
||||||
|
const jobId = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||||
|
|
||||||
// Filter out deleted items so they are actually removed
|
// Start Polling
|
||||||
|
this.startLogPolling(jobId);
|
||||||
|
|
||||||
|
// Expand Log Card
|
||||||
|
if (!ui.logCard.classList.contains('expanded')) {
|
||||||
|
ui.toggleLogExpand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out deleted items
|
||||||
const activeItems = state.items.filter(i => !i._deleted);
|
const activeItems = state.items.filter(i => !i._deleted);
|
||||||
|
|
||||||
// Send final state array to backend
|
// Send final state array to backend
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler((logs) => {
|
.withSuccessHandler((logs) => {
|
||||||
ui.saveBtn.innerText = "Saved!";
|
ui.saveBtn.innerText = "Saved!";
|
||||||
|
this.stopLogPolling(); // Stop polling
|
||||||
|
|
||||||
|
// Final sync of logs (in case polling missed the very end)
|
||||||
|
// But usually the returned logs are the full set or summary?
|
||||||
|
// The backend returns the full array. Let's merge or just ensure we show "Complete".
|
||||||
|
// Since we were polling, we might have partials.
|
||||||
|
// Let's just trust the stream has been showing progress.
|
||||||
|
// We can log a completion message.
|
||||||
|
ui.logStatus('save', 'Process Completed Successfully.', 'success');
|
||||||
|
|
||||||
// Verify logs is an array (backward compatibility check)
|
|
||||||
if (Array.isArray(logs)) {
|
|
||||||
document.getElementById('status-log-container').innerHTML = '';
|
|
||||||
logs.forEach(l => ui.logStatus('save', l, 'info'));
|
|
||||||
ui.toggleLog(true); // Force show
|
|
||||||
} else {
|
|
||||||
// Fallback for old backend
|
|
||||||
alert("Changes Saved & Synced!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload to get fresh IDs/State, preserving the save logs
|
// Reload to get fresh IDs/State, preserving the save logs
|
||||||
setTimeout(() => this.loadMedia(true), 1500);
|
setTimeout(() => this.loadMedia(true), 1500);
|
||||||
})
|
})
|
||||||
.withFailureHandler(e => {
|
.withFailureHandler(e => {
|
||||||
|
this.stopLogPolling();
|
||||||
alert(`Save Failed: ${e.message}`);
|
alert(`Save Failed: ${e.message}`);
|
||||||
|
ui.logStatus('fatal', `Save Failed: ${e.message}`, 'error');
|
||||||
ui.toggleSave(true);
|
ui.toggleSave(true);
|
||||||
})
|
})
|
||||||
.saveMediaChanges(state.sku, activeItems);
|
.saveMediaChanges(state.sku, activeItems, jobId);
|
||||||
|
},
|
||||||
|
|
||||||
|
logPollInterval: null,
|
||||||
|
knownLogCount: 0,
|
||||||
|
|
||||||
|
startLogPolling(jobId) {
|
||||||
|
if (this.logPollInterval) clearInterval(this.logPollInterval);
|
||||||
|
this.knownLogCount = 0;
|
||||||
|
|
||||||
|
this.logPollInterval = setInterval(() => {
|
||||||
|
google.script.run
|
||||||
|
.withSuccessHandler(logs => {
|
||||||
|
if (!logs || logs.length === 0) return;
|
||||||
|
|
||||||
|
// Append ONLY new logs
|
||||||
|
// Simple approach: standard loop since we know count
|
||||||
|
if (logs.length > this.knownLogCount) {
|
||||||
|
const newLogs = logs.slice(this.knownLogCount);
|
||||||
|
newLogs.forEach(l => ui.logStatus('stream', l));
|
||||||
|
this.knownLogCount = logs.length;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.pollJobLogs(jobId);
|
||||||
|
}, 1000); // Poll every second
|
||||||
|
},
|
||||||
|
|
||||||
|
stopLogPolling() {
|
||||||
|
if (this.logPollInterval) {
|
||||||
|
clearInterval(this.logPollInterval);
|
||||||
|
this.logPollInterval = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFiles(fileList) {
|
handleFiles(fileList) {
|
||||||
|
if (!fileList || fileList.length === 0) return;
|
||||||
|
this.setPickerState(true);
|
||||||
Array.from(fileList).forEach(file => {
|
Array.from(fileList).forEach(file => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
@ -1205,6 +1345,12 @@
|
|||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(() => {
|
.withSuccessHandler(() => {
|
||||||
this.loadMedia();
|
this.loadMedia();
|
||||||
|
this.setPickerState(false);
|
||||||
|
})
|
||||||
|
.withFailureHandler(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert("Upload failed: " + err.message);
|
||||||
|
this.setPickerState(false);
|
||||||
})
|
})
|
||||||
.saveFileToDrive(state.sku, file.name, file.type, data);
|
.saveFileToDrive(state.sku, file.name, file.type, data);
|
||||||
};
|
};
|
||||||
@ -1212,27 +1358,57 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Picker ---
|
setPickerState(isActive) {
|
||||||
// --- Picker ---
|
const btnDrive = document.getElementById('btn-upload-drive');
|
||||||
|
const btnPhotos = document.getElementById('btn-upload-photos');
|
||||||
|
const btnComp = document.getElementById('btn-upload-computer');
|
||||||
|
|
||||||
|
[btnDrive, btnPhotos, btnComp].forEach(btn => {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = isActive;
|
||||||
|
btn.style.opacity = isActive ? '0.5' : '1';
|
||||||
|
btn.style.cursor = isActive ? 'not-allowed' : 'pointer';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
openPicker() {
|
openPicker() {
|
||||||
if (!pickerApiLoaded) return alert("API Loading...");
|
if (!pickerApiLoaded) return alert("API Loading...");
|
||||||
google.script.run.withSuccessHandler(c => createPicker(c)).getPickerConfig();
|
this.setPickerState(true);
|
||||||
|
google.script.run
|
||||||
|
.withSuccessHandler(c => createPicker(c))
|
||||||
|
.withFailureHandler(e => {
|
||||||
|
alert("Failed to load picker: " + e.message);
|
||||||
|
this.setPickerState(false);
|
||||||
|
})
|
||||||
|
.getPickerConfig();
|
||||||
},
|
},
|
||||||
|
|
||||||
importFromPicker(fileId, mime, name, url) {
|
importFromPicker(fileId, mime, name, url) {
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(() => this.loadMedia())
|
.withSuccessHandler(() => {
|
||||||
|
this.loadMedia();
|
||||||
|
this.setPickerState(false);
|
||||||
|
})
|
||||||
|
.withFailureHandler(e => {
|
||||||
|
alert("Import failed: " + e.message);
|
||||||
|
this.setPickerState(false);
|
||||||
|
})
|
||||||
.importFromPicker(state.sku, fileId, mime, name, url);
|
.importFromPicker(state.sku, fileId, mime, name, url);
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Photos (Popup Flow) ---
|
|
||||||
startPhotoSession() {
|
startPhotoSession() {
|
||||||
|
this.setPickerState(true);
|
||||||
ui.updatePhotoStatus("Starting session...");
|
ui.updatePhotoStatus("Starting session...");
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(session => {
|
.withSuccessHandler(session => {
|
||||||
ui.showPhotoSession(session.pickerUri);
|
ui.showPhotoSession(session.pickerUri);
|
||||||
this.pollPhotoSession(session.id);
|
this.pollPhotoSession(session.id);
|
||||||
})
|
})
|
||||||
|
.withFailureHandler(e => {
|
||||||
|
ui.updatePhotoStatus("Failed: " + e.message);
|
||||||
|
this.setPickerState(false);
|
||||||
|
})
|
||||||
.createPhotoSession();
|
.createPhotoSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1245,13 +1421,18 @@
|
|||||||
if (res.status === 'complete') {
|
if (res.status === 'complete') {
|
||||||
processing = true;
|
processing = true;
|
||||||
ui.updatePhotoStatus("Importing photos...");
|
ui.updatePhotoStatus("Importing photos...");
|
||||||
controller.processPhotoItems(res.mediaItems);
|
this.processPhotoItems(res.mediaItems);
|
||||||
} else if (res.status === 'error') {
|
} else if (res.status === 'error') {
|
||||||
ui.updatePhotoStatus("Error: " + res.message);
|
ui.updatePhotoStatus("Error: " + res.message);
|
||||||
|
this.setPickerState(false);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(check, 2000);
|
setTimeout(check, 2000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.withFailureHandler(e => {
|
||||||
|
ui.updatePhotoStatus("Error polling: " + e.message);
|
||||||
|
this.setPickerState(false);
|
||||||
|
})
|
||||||
.checkPhotoSession(sessionId);
|
.checkPhotoSession(sessionId);
|
||||||
};
|
};
|
||||||
check();
|
check();
|
||||||
@ -1283,9 +1464,18 @@
|
|||||||
if (done === items.length) {
|
if (done === items.length) {
|
||||||
ui.updatePhotoStatus("Done!");
|
ui.updatePhotoStatus("Done!");
|
||||||
controller.loadMedia();
|
controller.loadMedia();
|
||||||
|
this.setPickerState(false);
|
||||||
setTimeout(() => ui.closePhotoSession(), 2000);
|
setTimeout(() => ui.closePhotoSession(), 2000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.withFailureHandler(e => {
|
||||||
|
console.error("Import failed", e);
|
||||||
|
// If last one
|
||||||
|
done++;
|
||||||
|
if (done === items.length) {
|
||||||
|
this.setPickerState(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
.importFromPicker(state.sku, null, mimeType, filename, url);
|
.importFromPicker(state.sku, null, mimeType, filename, url);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -1394,11 +1584,11 @@
|
|||||||
document.getElementById('btn-match-confirm').innerText = "Linking...";
|
document.getElementById('btn-match-confirm').innerText = "Linking...";
|
||||||
document.getElementById('btn-match-skip').disabled = true;
|
document.getElementById('btn-match-skip').disabled = true;
|
||||||
|
|
||||||
// ui.logStatus('link', 'Linking ' + match.drive.filename + '...', 'info');
|
ui.logStatus('link', 'Linking ' + match.drive.filename + '...', 'info');
|
||||||
|
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(function () {
|
.withSuccessHandler(function () {
|
||||||
// ui.logStatus('link', 'Linked ' + match.drive.filename, 'success');
|
ui.logStatus('link', 'Linked ' + match.drive.filename, 'success');
|
||||||
_this.nextMatch();
|
_this.nextMatch();
|
||||||
})
|
})
|
||||||
.withFailureHandler(function (e) {
|
.withFailureHandler(function (e) {
|
||||||
@ -1450,8 +1640,9 @@
|
|||||||
showGallery() {
|
showGallery() {
|
||||||
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';
|
||||||
|
document.getElementById('upload-section').style.display = 'block';
|
||||||
ui.logStatus('done', 'Finished loading.', 'success');
|
ui.logStatus('done', 'Finished loading.', 'success');
|
||||||
setTimeout(function () { ui.toggleLog(false); }, 1000);
|
// setTimeout(function () { ui.toggleLog(false); }, 1000); // Removed auto-hide
|
||||||
|
|
||||||
// Start Polling for Processing Items
|
// Start Polling for Processing Items
|
||||||
this.pollProcessingItems();
|
this.pollProcessingItems();
|
||||||
@ -1516,6 +1707,8 @@
|
|||||||
console.log("[MediaManager] Processing complete for " + item.filename);
|
console.log("[MediaManager] Processing complete for " + item.filename);
|
||||||
item.isProcessing = false;
|
item.isProcessing = false;
|
||||||
item.thumbnail = newItem.thumbnail;
|
item.thumbnail = newItem.thumbnail;
|
||||||
|
item.contentUrl = newItem.contentUrl; // Propagate URL
|
||||||
|
item.source = newItem.source; // Propagate source update (synced)
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1561,6 +1754,9 @@
|
|||||||
|
|
||||||
// Drive File (Always, since we removed Photos view)
|
// Drive File (Always, since we removed Photos view)
|
||||||
controller.importFromPicker(doc.id, doc.mimeType, doc.name, null);
|
controller.importFromPicker(doc.id, doc.mimeType, doc.name, null);
|
||||||
|
} else if (data.action == google.picker.Action.CANCEL) {
|
||||||
|
console.log("Picker cancelled");
|
||||||
|
controller.setPickerState(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@ -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, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia } from "./mediaHandlers"
|
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia, pollJobLogs } from "./mediaHandlers"
|
||||||
import { runSystemDiagnostics } from "./verificationSuite"
|
import { runSystemDiagnostics } from "./verificationSuite"
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@ -65,3 +65,4 @@ import { runSystemDiagnostics } from "./verificationSuite"
|
|||||||
;(global as any).checkPhotoSession = checkPhotoSession
|
;(global as any).checkPhotoSession = checkPhotoSession
|
||||||
;(global as any).debugFolderAccess = debugFolderAccess
|
;(global as any).debugFolderAccess = debugFolderAccess
|
||||||
;(global as any).linkDriveFileToShopifyMedia = linkDriveFileToShopifyMedia
|
;(global as any).linkDriveFileToShopifyMedia = linkDriveFileToShopifyMedia
|
||||||
|
;(global as any).pollJobLogs = pollJobLogs
|
||||||
|
|||||||
@ -23,8 +23,16 @@ jest.mock("./config", () => {
|
|||||||
jest.mock("./services/GASNetworkService")
|
jest.mock("./services/GASNetworkService")
|
||||||
jest.mock("./services/ShopifyMediaService")
|
jest.mock("./services/ShopifyMediaService")
|
||||||
jest.mock("./shopifyApi", () => ({ Shop: jest.fn() }))
|
jest.mock("./shopifyApi", () => ({ Shop: jest.fn() }))
|
||||||
jest.mock("./services/MediaService")
|
jest.mock("./services/MediaService", () => {
|
||||||
jest.mock("./Product", () => ({ Product: jest.fn().mockImplementation(() => ({ shopify_id: "123", MatchToShopifyProduct: jest.fn() })) }))
|
return {
|
||||||
|
MediaService: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
getUnifiedMediaState: jest.fn().mockReturnValue([]),
|
||||||
|
processMediaChanges: jest.fn().mockReturnValue([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// Mock GASDriveService
|
// Mock GASDriveService
|
||||||
@ -49,10 +57,30 @@ jest.mock("./services/GASSpreadsheetService", () => {
|
|||||||
GASSpreadsheetService: jest.fn().mockImplementation(() => {
|
GASSpreadsheetService: jest.fn().mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
getCellValueByColumnName: jest.fn().mockImplementation((sheet, row, col) => {
|
getCellValueByColumnName: jest.fn().mockImplementation((sheet, row, col) => {
|
||||||
|
// console.log(`Mock GASSpreadsheetService getCellValueByColumnName called: ${col}`);
|
||||||
if (col === "sku") return "TEST-SKU"
|
if (col === "sku") return "TEST-SKU"
|
||||||
if (col === "title") return "Test Product Title"
|
if (col === "title") return "Test Product Title"
|
||||||
return null
|
return null
|
||||||
})
|
}),
|
||||||
|
getRowNumberByColumnValue: jest.fn().mockReturnValue(5),
|
||||||
|
setCellValueByColumnName: jest.fn(),
|
||||||
|
getHeaders: jest.fn().mockReturnValue(["sku", "title", "thumbnail"]),
|
||||||
|
getRowData: jest.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock Product
|
||||||
|
jest.mock("./Product", () => {
|
||||||
|
return {
|
||||||
|
Product: jest.fn().mockImplementation((sku) => {
|
||||||
|
return {
|
||||||
|
sku: sku,
|
||||||
|
shopify_id: "shopify_id_123",
|
||||||
|
title: "Test Product Title",
|
||||||
|
MatchToShopifyProduct: jest.fn(),
|
||||||
|
ImportFromInventory: jest.fn()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -88,7 +116,13 @@ global.SpreadsheetApp = {
|
|||||||
getName: jest.fn().mockReturnValue("product_inventory"),
|
getName: jest.fn().mockReturnValue("product_inventory"),
|
||||||
getActiveRange: jest.fn().mockReturnValue({ getRow: () => 2 })
|
getActiveRange: jest.fn().mockReturnValue({ getRow: () => 2 })
|
||||||
}),
|
}),
|
||||||
getActive: jest.fn()
|
getActive: jest.fn(),
|
||||||
|
newCellImage: jest.fn().mockReturnValue({
|
||||||
|
setSourceUrl: jest.fn().mockReturnThis(),
|
||||||
|
setAltTextTitle: jest.fn().mockReturnThis(),
|
||||||
|
setAltTextDescription: jest.fn().mockReturnThis(),
|
||||||
|
build: jest.fn().mockReturnValue("CELL_IMAGE_OBJECT")
|
||||||
|
})
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
// UrlFetchApp
|
// UrlFetchApp
|
||||||
@ -132,10 +166,25 @@ global.Session = {
|
|||||||
global.HtmlService = {
|
global.HtmlService = {
|
||||||
createHtmlOutputFromFile: jest.fn().mockReturnValue({
|
createHtmlOutputFromFile: jest.fn().mockReturnValue({
|
||||||
setTitle: jest.fn().mockReturnThis(),
|
setTitle: jest.fn().mockReturnThis(),
|
||||||
setWidth: jest.fn().mockReturnThis()
|
setWidth: jest.fn().mockReturnThis(),
|
||||||
|
setHeight: jest.fn().mockReturnThis()
|
||||||
})
|
})
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
|
// MimeType
|
||||||
|
global.MimeType = {
|
||||||
|
JPEG: "image/jpeg",
|
||||||
|
PNG: "image/png"
|
||||||
|
} as any
|
||||||
|
|
||||||
|
// Mock CacheService for log streaming
|
||||||
|
global.CacheService = {
|
||||||
|
getDocumentCache: () => ({
|
||||||
|
get: (key) => null,
|
||||||
|
put: (k, v, t) => {},
|
||||||
|
remove: (k) => {}
|
||||||
|
})
|
||||||
|
} as any
|
||||||
|
|
||||||
describe("mediaHandlers", () => {
|
describe("mediaHandlers", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -249,7 +298,8 @@ describe("mediaHandlers", () => {
|
|||||||
|
|
||||||
// Get the instance that was created
|
// Get the instance that was created
|
||||||
const MockMediaService = MediaService as unknown as jest.Mock
|
const MockMediaService = MediaService as unknown as jest.Mock
|
||||||
const mockInstance = MockMediaService.mock.instances[MockMediaService.mock.instances.length - 1]
|
expect(MockMediaService).toHaveBeenCalled()
|
||||||
|
const mockInstance = MockMediaService.mock.results[MockMediaService.mock.results.length - 1].value
|
||||||
|
|
||||||
// Checking delegation
|
// Checking delegation
|
||||||
expect(mockInstance.getUnifiedMediaState).toHaveBeenCalledWith("SKU123", expect.anything())
|
expect(mockInstance.getUnifiedMediaState).toHaveBeenCalledWith("SKU123", expect.anything())
|
||||||
@ -263,16 +313,55 @@ describe("mediaHandlers", () => {
|
|||||||
saveMediaChanges("SKU123", finalState)
|
saveMediaChanges("SKU123", finalState)
|
||||||
|
|
||||||
const MockMediaService = MediaService as unknown as jest.Mock
|
const MockMediaService = MediaService as unknown as jest.Mock
|
||||||
const mockInstance = MockMediaService.mock.instances[MockMediaService.mock.instances.length - 1]
|
const mockInstance = MockMediaService.mock.results[MockMediaService.mock.results.length - 1].value
|
||||||
expect(mockInstance.processMediaChanges).toHaveBeenCalledWith("SKU123", finalState, expect.anything())
|
|
||||||
|
expect(mockInstance.processMediaChanges).toHaveBeenCalledWith("SKU123", finalState, expect.anything(), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should throw if product not synced", () => {
|
test("should throw if product not synced", () => {
|
||||||
const { Product } = require("./Product")
|
const MockProduct = Product as unknown as jest.Mock
|
||||||
Product.mockImplementationOnce(() => ({ shopify_id: null, MatchToShopifyProduct: jest.fn() }))
|
MockProduct.mockImplementationOnce(() => ({
|
||||||
|
shopify_id: null,
|
||||||
|
MatchToShopifyProduct: jest.fn(),
|
||||||
|
ImportFromInventory: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
expect(() => saveMediaChanges("SKU123", [])).toThrow("Product must be synced")
|
expect(() => saveMediaChanges("SKU123", [])).toThrow("Product must be synced")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should update sheet thumbnail with first image", () => {
|
||||||
|
// Setup mock MediaService to NOT throw and just return logs
|
||||||
|
const MockMediaService = MediaService as unknown as jest.Mock
|
||||||
|
const mockGetUnifiedMediaState = jest.fn().mockReturnValue([
|
||||||
|
{ id: "2", driveId: "drive_file_2", galleryOrder: 1, contentUrl: "https://cdn.shopify.com/test.jpg", thumbnail: "https://cdn.shopify.com/test.jpg" }
|
||||||
|
])
|
||||||
|
MockMediaService.mockImplementation(() => ({
|
||||||
|
processMediaChanges: jest.fn().mockReturnValue(["Log 1"]),
|
||||||
|
getUnifiedMediaState: mockGetUnifiedMediaState
|
||||||
|
}))
|
||||||
|
|
||||||
|
const finalState = [
|
||||||
|
{ id: "1", driveId: "drive_file_1", galleryOrder: 10 },
|
||||||
|
{ id: "2", driveId: "drive_file_2", galleryOrder: 1 } // Should be first
|
||||||
|
]
|
||||||
|
|
||||||
|
const logs = saveMediaChanges("TEST-SKU", finalState)
|
||||||
|
|
||||||
|
// Logs are now just passed through from MediaService since we commented out local log appending
|
||||||
|
expect(logs).toEqual(["Log 1"])
|
||||||
|
|
||||||
|
// Verify spreadsheet service interaction
|
||||||
|
const MockSpreadsheet = GASSpreadsheetService as unknown as jest.Mock
|
||||||
|
expect(MockSpreadsheet).toHaveBeenCalled()
|
||||||
|
|
||||||
|
const mockSS = MockSpreadsheet.mock.results[MockSpreadsheet.mock.results.length - 1].value
|
||||||
|
expect(mockSS.setCellValueByColumnName).toHaveBeenCalledWith(
|
||||||
|
"product_inventory",
|
||||||
|
5,
|
||||||
|
"thumbnail",
|
||||||
|
"CELL_IMAGE_OBJECT"
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Photo Session API", () => {
|
describe("Photo Session API", () => {
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export function getMediaForSku(sku: string): any[] {
|
|||||||
return mediaService.getUnifiedMediaState(sku, shopifyId)
|
return mediaService.getUnifiedMediaState(sku, shopifyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveMediaChanges(sku: string, finalState: any[]) {
|
export function saveMediaChanges(sku: string, finalState: any[], jobId: string | null = null) {
|
||||||
const config = new Config()
|
const config = new Config()
|
||||||
const driveService = new GASDriveService()
|
const driveService = new GASDriveService()
|
||||||
const shop = new Shop()
|
const shop = new Shop()
|
||||||
@ -84,7 +84,64 @@ export function saveMediaChanges(sku: string, finalState: any[]) {
|
|||||||
throw new Error("Product must be synced to Shopify before saving media changes.")
|
throw new Error("Product must be synced to Shopify before saving media changes.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaService.processMediaChanges(sku, finalState, product.shopify_id)
|
const logs = mediaService.processMediaChanges(sku, finalState, product.shopify_id, jobId)
|
||||||
|
|
||||||
|
// Update Sheet Thumbnail (Top of Gallery)
|
||||||
|
try {
|
||||||
|
// Refresh state to get Shopify CDN URLs
|
||||||
|
const latestState = mediaService.getUnifiedMediaState(sku, product.shopify_id);
|
||||||
|
const sorted = latestState.sort((a, b) => (a.galleryOrder || 0) - (b.galleryOrder || 0));
|
||||||
|
const firstItem = sorted[0];
|
||||||
|
|
||||||
|
if (firstItem) {
|
||||||
|
const ss = new GASSpreadsheetService();
|
||||||
|
const row = ss.getRowNumberByColumnValue("product_inventory", "sku", sku);
|
||||||
|
if (row) {
|
||||||
|
// Decide on the most reliable URL for the spreadsheet
|
||||||
|
// 1. If it's a synced Shopify item, use the Shopify preview image URL (public)
|
||||||
|
// 2. Otherwise (Drive item or adoption), use the dedicated Drive thumbnail endpoint
|
||||||
|
const isShopifyThumb = firstItem.thumbnail && firstItem.thumbnail.startsWith('http');
|
||||||
|
const driveThumbUrl = `https://drive.google.com/thumbnail?id=${firstItem.driveId}&sz=w400`;
|
||||||
|
const thumbUrl = isShopifyThumb ? firstItem.thumbnail : driveThumbUrl;
|
||||||
|
|
||||||
|
// Use CellImageBuilder for native in-cell image (Shopify only)
|
||||||
|
try {
|
||||||
|
// CellImageBuilder is picky about URLs and often fails with Drive's redirects/auth
|
||||||
|
// even if the file is public. Formula-based IMAGE() is more robust for Drive.
|
||||||
|
if (!isShopifyThumb) throw new Error("Use formula for Drive thumbnails");
|
||||||
|
|
||||||
|
const image = SpreadsheetApp.newCellImage()
|
||||||
|
.setSourceUrl(thumbUrl)
|
||||||
|
.setAltTextTitle(sku)
|
||||||
|
.setAltTextDescription(`Thumbnail for ${sku}`)
|
||||||
|
.build();
|
||||||
|
ss.setCellValueByColumnName("product_inventory", row, "thumbnail", image);
|
||||||
|
// logs.push(`Updated sheet thumbnail for SKU ${sku}`); // Logs array is static now, won't stream this unless we refactor sheet update to use log() too. User cares mostly about main process.
|
||||||
|
} catch (builderErr) {
|
||||||
|
// Fallback to formula
|
||||||
|
ss.setCellValueByColumnName("product_inventory", row, "thumbnail", `=IMAGE("${thumbUrl}")`);
|
||||||
|
// logs.push(`Updated sheet thumbnail (Formula) for SKU ${sku}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// logs.push(`Warning: Could not find row for SKU ${sku} to update thumbnail.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to update sheet thumbnail", e);
|
||||||
|
// logs.push(`Warning: Failed to update sheet thumbnail: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pollJobLogs(jobId: string): string[] {
|
||||||
|
try {
|
||||||
|
const cache = CacheService.getDocumentCache();
|
||||||
|
const json = cache.get(`job_logs_${jobId}`);
|
||||||
|
return json ? JSON.parse(json) : [];
|
||||||
|
} catch(e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,15 @@ describe("MediaService Robust Sync", () => {
|
|||||||
removeFile: (f) => {}
|
removeFile: (f) => {}
|
||||||
})
|
})
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
|
// Mock CacheService for log streaming
|
||||||
|
global.CacheService = {
|
||||||
|
getDocumentCache: () => ({
|
||||||
|
get: (key) => null,
|
||||||
|
put: (k, v, t) => {},
|
||||||
|
remove: (k) => {}
|
||||||
|
})
|
||||||
|
} as any
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Strict Matching: Only matches via property, ignores filename", () => {
|
test("Strict Matching: Only matches via property, ignores filename", () => {
|
||||||
@ -304,4 +313,29 @@ describe("MediaService Robust Sync", () => {
|
|||||||
expect(item.isProcessing).toBe(true)
|
expect(item.isProcessing).toBe(true)
|
||||||
expect(item.thumbnail).toContain("data:image/svg+xml;base64")
|
expect(item.thumbnail).toContain("data:image/svg+xml;base64")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Processing: Marks item as processing if Shopify status is PROCESSING", () => {
|
||||||
|
const folder = driveService.getOrCreateFolder("SKU_SHOP_PROCESS", "root")
|
||||||
|
|
||||||
|
// Drive File
|
||||||
|
const blob = { getName: () => "vid.mp4", getBytes: () => [], getMimeType: () => "video/mp4", getThumbnail: () => ({ getBytes: () => [] }) } as any
|
||||||
|
const f = driveService.saveFile(blob, folder.getId())
|
||||||
|
driveService.updateFileProperties(f.getId(), { shopify_media_id: "gid://shopify/Media/Proc1" })
|
||||||
|
|
||||||
|
// Shopify Media (Processing)
|
||||||
|
shopifyService.getProductMedia = jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "gid://shopify/Media/Proc1",
|
||||||
|
filename: "vid.mp4",
|
||||||
|
mediaContentType: "VIDEO",
|
||||||
|
status: "PROCESSING",
|
||||||
|
preview: { image: { originalSrc: null } } // Preview might be missing during processing
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const state = mediaService.getUnifiedMediaState("SKU_SHOP_PROCESS", "pid")
|
||||||
|
const item = state.find(s => s.id === f.getId())
|
||||||
|
|
||||||
|
expect(item.isProcessing).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -24,11 +24,38 @@ export class MediaService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private logToCache(jobId: string, message: string) {
|
||||||
|
if (!jobId) return;
|
||||||
|
try {
|
||||||
|
const cache = CacheService.getDocumentCache();
|
||||||
|
const key = `job_logs_${jobId}`;
|
||||||
|
const existing = cache.get(key);
|
||||||
|
let logs = existing ? JSON.parse(existing) : [];
|
||||||
|
logs.push(message);
|
||||||
|
// Expire in 10 minutes (plenty for a save operation)
|
||||||
|
cache.put(key, JSON.stringify(logs), 600);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Retrying log to cache failed slightly", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getDiagnostics(sku: string, shopifyProductId: string) {
|
getDiagnostics(sku: string, shopifyProductId: string) {
|
||||||
const results = {
|
const results = {
|
||||||
drive: { status: 'pending', fileCount: 0, folderId: null, folderUrl: null, error: null },
|
drive: { status: 'pending', fileCount: 0, folderId: null, folderUrl: null, error: null },
|
||||||
shopify: { status: 'pending', mediaCount: 0, id: shopifyProductId, adminUrl: null, error: null },
|
shopify: { status: 'pending', mediaCount: 0, id: shopifyProductId, adminUrl: null, error: null },
|
||||||
matching: { status: 'pending', error: null }
|
matching: { status: 'pending', error: null },
|
||||||
|
activeJobId: null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Active Job
|
||||||
|
try {
|
||||||
|
const cache = CacheService.getDocumentCache();
|
||||||
|
const activeJobId = cache.get(`active_job_${sku}`);
|
||||||
|
if (activeJobId) {
|
||||||
|
results.activeJobId = activeJobId;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to check active job", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Unsafe Drive Check
|
// 1. Unsafe Drive Check
|
||||||
@ -106,6 +133,8 @@ export class MediaService {
|
|||||||
if (props['custom_thumbnail_id']) customThumbnailId = props['custom_thumbnail_id'];
|
if (props['custom_thumbnail_id']) customThumbnailId = props['custom_thumbnail_id'];
|
||||||
if (props['parent_video_id']) parentVideoId = props['parent_video_id'];
|
if (props['parent_video_id']) parentVideoId = props['parent_video_id'];
|
||||||
|
|
||||||
|
console.log(`[DEBUG] File ${f.getName()} Props:`, JSON.stringify(props));
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to get properties for ${f.getName()}`)
|
console.warn(`Failed to get properties for ${f.getName()}`)
|
||||||
}
|
}
|
||||||
@ -239,6 +268,21 @@ export class MediaService {
|
|||||||
console.log(`[MediaService] Using Sidecar Thumbnail for ${d.file.getName()}`);
|
console.log(`[MediaService] Using Sidecar Thumbnail for ${d.file.getName()}`);
|
||||||
thumbnail = sidecarThumbMap.get(d.file.getId()) || "";
|
thumbnail = sidecarThumbMap.get(d.file.getId()) || "";
|
||||||
isProcessing = true; // SHOW HOURGLASS (Request #3)
|
isProcessing = true; // SHOW HOURGLASS (Request #3)
|
||||||
|
} else if (match && (
|
||||||
|
match.status === 'PROCESSING' ||
|
||||||
|
match.status === 'UPLOADED' ||
|
||||||
|
(match.mediaContentType === 'VIDEO' && (!match.sources || match.sources.length === 0) && match.status !== 'FAILED')
|
||||||
|
)) {
|
||||||
|
// Shopify Processing (Explicit Status OR Ready-but-missing-sources)
|
||||||
|
console.log(`[MediaService] Shopify Media is Processing: ${d.file.getName()} (Status: ${match.status}, Sources: ${match.sources ? match.sources.length : 0})`);
|
||||||
|
isProcessing = true;
|
||||||
|
// Use Drive thumb as fallback if Shopify preview not ready
|
||||||
|
if (!thumbnail) {
|
||||||
|
try {
|
||||||
|
const nativeThumb = `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`;
|
||||||
|
if (nativeThumb.length > 100) thumbnail = nativeThumb;
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 2. Native / Fallback
|
// 2. Native / Fallback
|
||||||
try {
|
try {
|
||||||
@ -331,10 +375,24 @@ export class MediaService {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
processMediaChanges(sku: string, finalState: any[], shopifyProductId: string): string[] {
|
processMediaChanges(sku: string, finalState: any[], shopifyProductId: string, jobId: string | null = null): string[] {
|
||||||
const logs: string[] = []
|
const logs: string[] = []
|
||||||
logs.push(`Starting processing for SKU ${sku}`)
|
|
||||||
console.log(`MediaService: Processing changes for SKU ${sku}`)
|
// Helper to log to both return array and cache
|
||||||
|
const log = (msg: string) => {
|
||||||
|
logs.push(msg);
|
||||||
|
console.log(msg);
|
||||||
|
if (jobId) this.logToCache(jobId, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Starting processing for SKU ${sku}`)
|
||||||
|
|
||||||
|
// Register Job
|
||||||
|
if (jobId) {
|
||||||
|
try {
|
||||||
|
CacheService.getDocumentCache().put(`active_job_${sku}`, jobId, 600);
|
||||||
|
} catch(e) { console.warn("Failed to register active job", e); }
|
||||||
|
}
|
||||||
|
|
||||||
// 0. Service Availability Check & Local Capture (Fixing 'undefined' context issues)
|
// 0. Service Availability Check & Local Capture (Fixing 'undefined' context issues)
|
||||||
const shopifySvc = this.shopifyMediaService
|
const shopifySvc = this.shopifyMediaService
|
||||||
@ -349,15 +407,14 @@ export class MediaService {
|
|||||||
|
|
||||||
// 2. Process Deletions (Orphans not in final state are removed from Shopify)
|
// 2. Process Deletions (Orphans not in final state are removed from Shopify)
|
||||||
const toDelete = currentState.filter(c => !finalIds.has(c.id))
|
const toDelete = currentState.filter(c => !finalIds.has(c.id))
|
||||||
if (toDelete.length === 0) logs.push("No deletions found.")
|
if (toDelete.length === 0) log("No deletions found.")
|
||||||
|
|
||||||
toDelete.forEach(item => {
|
toDelete.forEach(item => {
|
||||||
const msg = `Deleting item: ${item.filename}`
|
const msg = `Deleting item: ${item.filename}`
|
||||||
logs.push(msg)
|
log(msg)
|
||||||
console.log(msg)
|
|
||||||
if (item.shopifyId) {
|
if (item.shopifyId) {
|
||||||
shopifySvc.productDeleteMedia(shopifyProductId, item.shopifyId)
|
shopifySvc.productDeleteMedia(shopifyProductId, item.shopifyId)
|
||||||
logs.push(`- Deleted from Shopify (${item.shopifyId})`)
|
log(`- Deleted from Shopify (${item.shopifyId})`)
|
||||||
}
|
}
|
||||||
if (item.driveId) {
|
if (item.driveId) {
|
||||||
// Check for Associated Sidecar Thumbs (Request #2)
|
// Check for Associated Sidecar Thumbs (Request #2)
|
||||||
@ -372,14 +429,14 @@ export class MediaService {
|
|||||||
const props = driveSvc.getFileProperties(item.driveId);
|
const props = driveSvc.getFileProperties(item.driveId);
|
||||||
if (props && props['custom_thumbnail_id']) {
|
if (props && props['custom_thumbnail_id']) {
|
||||||
driveSvc.trashFile(props['custom_thumbnail_id']);
|
driveSvc.trashFile(props['custom_thumbnail_id']);
|
||||||
logs.push(`- Trashed associated Sidecar Thumbnail (${props['custom_thumbnail_id']})`);
|
log(`- Trashed associated Sidecar Thumbnail (${props['custom_thumbnail_id']})`);
|
||||||
}
|
}
|
||||||
} catch (ignore) {
|
} catch (ignore) {
|
||||||
// If file already gone or other error
|
// If file already gone or other error
|
||||||
}
|
}
|
||||||
|
|
||||||
driveSvc.trashFile(item.driveId)
|
driveSvc.trashFile(item.driveId)
|
||||||
logs.push(`- Trashed in Drive (${item.driveId})`)
|
log(`- Trashed in Drive (${item.driveId})`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -389,8 +446,7 @@ export class MediaService {
|
|||||||
finalState.forEach(item => {
|
finalState.forEach(item => {
|
||||||
if (item.source === 'shopify_only' && item.shopifyId) {
|
if (item.source === 'shopify_only' && item.shopifyId) {
|
||||||
const msg = `Adopting Orphan: ${item.filename}`
|
const msg = `Adopting Orphan: ${item.filename}`
|
||||||
logs.push(msg)
|
log(msg)
|
||||||
console.log(msg)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Download
|
// Download
|
||||||
@ -416,9 +472,9 @@ export class MediaService {
|
|||||||
// Update item refs for subsequent steps
|
// Update item refs for subsequent steps
|
||||||
item.driveId = file.getId()
|
item.driveId = file.getId()
|
||||||
item.source = 'synced'
|
item.source = 'synced'
|
||||||
logs.push(`- Adopted to Drive (${file.getId()})`)
|
log(`- Adopted to Drive (${file.getId()})`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logs.push(`- Failed to adopt ${item.filename}: ${e}`)
|
log(`- Failed to adopt ${item.filename}: ${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -427,7 +483,7 @@ export class MediaService {
|
|||||||
const toUpload = finalState.filter(item => item.source === 'drive_only' && item.driveId)
|
const toUpload = finalState.filter(item => item.source === 'drive_only' && item.driveId)
|
||||||
if (toUpload.length > 0) {
|
if (toUpload.length > 0) {
|
||||||
const msg = `Uploading ${toUpload.length} new items from Drive`
|
const msg = `Uploading ${toUpload.length} new items from Drive`
|
||||||
logs.push(msg)
|
log(msg)
|
||||||
const uploads = toUpload.map(item => {
|
const uploads = toUpload.map(item => {
|
||||||
const f = driveSvc.getFileById(item.driveId)
|
const f = driveSvc.getFileById(item.driveId)
|
||||||
return {
|
return {
|
||||||
@ -454,7 +510,7 @@ export class MediaService {
|
|||||||
|
|
||||||
if (stagedResp.userErrors && stagedResp.userErrors.length > 0) {
|
if (stagedResp.userErrors && stagedResp.userErrors.length > 0) {
|
||||||
console.error("[MediaService] stagedUploadsCreate Errors:", JSON.stringify(stagedResp.userErrors))
|
console.error("[MediaService] stagedUploadsCreate Errors:", JSON.stringify(stagedResp.userErrors))
|
||||||
logs.push(`- Upload preparation failed: ${stagedResp.userErrors.map(e => e.message).join(', ')}`)
|
log(`- Upload preparation failed: ${stagedResp.userErrors.map(e => e.message).join(', ')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const targets = stagedResp.stagedTargets
|
const targets = stagedResp.stagedTargets
|
||||||
@ -463,7 +519,7 @@ export class MediaService {
|
|||||||
uploads.forEach((u, i) => {
|
uploads.forEach((u, i) => {
|
||||||
const target = targets[i]
|
const target = targets[i]
|
||||||
if (!target || !target.url) {
|
if (!target || !target.url) {
|
||||||
logs.push(`- Failed to get upload target for ${u.filename}: Invalid target`)
|
log(`- Failed to get upload target for ${u.filename}: Invalid target`)
|
||||||
console.warn(`[MediaService] Missing target URL for ${u.filename}. Target:`, JSON.stringify(target))
|
console.warn(`[MediaService] Missing target URL for ${u.filename}. Target:`, JSON.stringify(target))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -490,7 +546,7 @@ export class MediaService {
|
|||||||
driveSvc.updateFileProperties(originalItem.driveId, { shopify_media_id: m.id })
|
driveSvc.updateFileProperties(originalItem.driveId, { shopify_media_id: m.id })
|
||||||
originalItem.shopifyId = m.id
|
originalItem.shopifyId = m.id
|
||||||
originalItem.source = 'synced'
|
originalItem.source = 'synced'
|
||||||
logs.push(`- Created in Shopify (${m.id}) and linked`)
|
log(`- Created in Shopify (${m.id}) and linked`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -524,7 +580,7 @@ export class MediaService {
|
|||||||
const timestamp = new Date().getTime()
|
const timestamp = new Date().getTime()
|
||||||
const newName = `${sku}_${timestamp}.${ext}`
|
const newName = `${sku}_${timestamp}.${ext}`
|
||||||
driveSvc.renameFile(item.driveId, newName)
|
driveSvc.renameFile(item.driveId, newName)
|
||||||
logs.push(`- Renamed ${currentName} -> ${newName} (Non-conforming)`)
|
log(`- Renamed ${currentName} -> ${newName} (Non-conforming)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// C. Prepare Shopify Reorder
|
// C. Prepare Shopify Reorder
|
||||||
@ -533,17 +589,25 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logs.push(`- Error updating ${item.filename}: ${e}`)
|
log(`- Error updating ${item.filename}: ${e}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 6. Execute Shopify Reorder
|
// 6. Execute Shopify Reorder
|
||||||
if (reorderMoves.length > 0) {
|
if (reorderMoves.length > 0) {
|
||||||
shopifySvc.productReorderMedia(shopifyProductId, reorderMoves)
|
shopifySvc.productReorderMedia(shopifyProductId, reorderMoves)
|
||||||
logs.push("Reordered media in Shopify.")
|
log("Reordered media in Shopify.")
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Processing Complete.")
|
||||||
|
|
||||||
|
// Clear Job (Success)
|
||||||
|
if (jobId) {
|
||||||
|
try {
|
||||||
|
CacheService.getDocumentCache().remove(`active_job_${sku}`);
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
logs.push("Processing Complete.")
|
|
||||||
return logs
|
return logs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export class ShopifyMediaService implements IShopifyMediaService {
|
|||||||
id
|
id
|
||||||
alt
|
alt
|
||||||
mediaContentType
|
mediaContentType
|
||||||
|
status
|
||||||
preview {
|
preview {
|
||||||
image {
|
image {
|
||||||
originalSrc
|
originalSrc
|
||||||
|
|||||||
BIN
test_output.txt
BIN
test_output.txt
Binary file not shown.
Reference in New Issue
Block a user