← back to Japan Enrich
japan viewer: import-queue drawer — list queued SKUs, per-row remove, clear all
9f2be431a2a42c7ed006186ea2e26b53602c8d8b · 2026-07-06 13:49:18 -0700 · Steve
Click the '⤓ N queued' badge to open a right-side drawer (thumb + sku +
distributor + title per row). Per-row ✕ removes; 'Clear all' empties the queue.
/api/import-queue now returns full items; added POST /api/import/clear. Panel
stays in sync with grid card chips (syncCardImp) and refreshes if open when you
queue from a card or bulk-import.
Files touched
M viewer-local/public/index.htmlM viewer-local/server.js
Diff
commit 9f2be431a2a42c7ed006186ea2e26b53602c8d8b
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 6 13:49:18 2026 -0700
japan viewer: import-queue drawer — list queued SKUs, per-row remove, clear all
Click the '⤓ N queued' badge to open a right-side drawer (thumb + sku +
distributor + title per row). Per-row ✕ removes; 'Clear all' empties the queue.
/api/import-queue now returns full items; added POST /api/import/clear. Panel
stays in sync with grid card chips (syncCardImp) and refreshes if open when you
queue from a card or bulk-import.
---
viewer-local/public/index.html | 82 ++++++++++++++++++++++++++++++++++++++++--
viewer-local/server.js | 6 +++-
2 files changed, 85 insertions(+), 3 deletions(-)
diff --git a/viewer-local/public/index.html b/viewer-local/public/index.html
index 395a00d..9bf120e 100644
--- a/viewer-local/public/index.html
+++ b/viewer-local/public/index.html
@@ -94,7 +94,29 @@ body.imgonly .grid:not(.list) .card .b{display:none}
.chip.imp{background:transparent;color:var(--mut);border-color:var(--line);cursor:pointer;user-select:none}
.chip.imp:hover{border-color:var(--acc);color:var(--ink)}
.chip.imp.on{border-color:var(--pub);color:var(--pub)}
-#qbadge{color:var(--pub);font-weight:600}
+#qbadge{color:var(--pub);font-weight:600;cursor:pointer}
+#qbadge:hover{text-decoration:underline}
+/* ── import-queue drawer (right side) ── */
+#qpanel{position:fixed;top:0;right:0;z-index:60;width:360px;max-width:92vw;height:100vh;background:#0e0f15;
+ border-left:1px solid var(--line);box-shadow:-8px 0 24px #0006;display:flex;flex-direction:column}
+#qpanel[hidden]{display:none}
+.qp-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--line)}
+.qp-head strong{font-size:14px;flex:1}
+.qp-head button{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:5px 10px;cursor:pointer;font-size:12px}
+.qp-head #qp-clear:hover{border-color:#c0392b;color:#e57373}
+.qp-head #qp-close:hover{border-color:var(--acc)}
+#qp-list{flex:1;overflow:auto;padding:8px 10px 20px}
+.qi{display:grid;grid-template-columns:40px 1fr 22px;gap:9px;align-items:center;padding:6px 4px;border-bottom:1px solid var(--line)}
+.qi img{width:40px;height:40px;object-fit:cover;border-radius:4px;background:#0a0b0f;display:block}
+.qi .qm{min-width:0}
+.qi .qs{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--mut)}
+.qi .qt{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.qi .qd{font-size:10px;color:var(--acc)}
+.qi .qd.direct{color:var(--mut)}
+.qi .qx{background:transparent;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;width:22px;height:22px;line-height:1;padding:0}
+.qi .qx:hover{border-color:#c0392b;color:#e57373}
+.qp-empty{padding:40px 12px;text-align:center;color:var(--mut);font-size:12.5px}
+.qp-foot{padding:10px 14px;border-top:1px solid var(--line);font-size:11px;line-height:1.5}
.chip.toggle{background:transparent;color:var(--mut);border-color:var(--line);cursor:pointer;user-select:none}
.chip.toggle:hover{border-color:var(--acc);color:var(--ink)}
/* colorway sibling dots — the "other colors of this pattern" */
@@ -190,6 +212,15 @@ body.listmode #listhead{display:grid}
<div id="empty" style="display:none">No SKUs match.</div>
</main>
</div>
+<div id="qpanel" hidden aria-label="import queue">
+ <div class="qp-head">
+ <strong>Import queue (<span id="qp-count">0</span>)</strong>
+ <button id="qp-clear" type="button">Clear all</button>
+ <button id="qp-close" type="button" aria-label="Close">✕</button>
+ </div>
+ <div id="qp-list"></div>
+ <div class="qp-foot muted">Queued SKUs stage into <code>dw_unified</code> via <code>db/promote-import-queue.js</code>. Promotion & Shopify publish are gated.</div>
+</div>
<script>
const $=s=>document.querySelector(s);
const grid=$('#grid');
@@ -312,7 +343,52 @@ grid.addEventListener('click',e=>{
if(c&&c.dataset.href)window.open(c.dataset.href,'_blank','noopener,noreferrer');
});
// ── import queue: enqueue INTENT for onboarding (promotion to dw_unified/Shopify is gated) ──
-function updateQueueBadge(){const el=$('#qbadge');if(el)el.textContent=QUEUED.size?`⤓ ${QUEUED.size.toLocaleString()} queued`:'';}
+function updateQueueBadge(){const el=$('#qbadge');if(el)el.textContent=`⤓ ${QUEUED.size.toLocaleString()} queued`;}
+// keep a grid card's import chip in sync when the queue changes from the panel
+function syncCardImp(sku,queued){
+ const el=document.querySelector(`.chip.imp[data-sku="${(window.CSS&&CSS.escape)?CSS.escape(sku):sku}"]`);
+ if(el){el.classList.toggle('on',queued);el.textContent=queued?'✓ queued':'⤓ import';}
+}
+// ── import-queue drawer ──
+const qpanel=()=>$('#qpanel');
+async function openQueue(){
+ const d=await fetch(location.origin+'/api/import-queue').then(r=>r.json()).catch(()=>({items:[]}));
+ QUEUED=new Set(d.skus||[]); updateQueueBadge();
+ renderQueue(d.items||[]);
+ qpanel().hidden=false;
+}
+function renderQueue(items){
+ $('#qp-count').textContent=items.length.toLocaleString();
+ const list=$('#qp-list');
+ if(!items.length){list.innerHTML='<div class="qp-empty">Queue is empty.<br>Click <b>⤓ import</b> on any card to add SKUs.</div>';return;}
+ list.innerHTML=items.map(r=>{
+ const img=r.image?`<img loading="lazy" src="${esc(r.image)}" onerror="this.style.visibility='hidden'">`:'<img>';
+ const d=r.distributor?`<div class="qd${r.dist_direct?' direct':''}">${r.dist_direct?'':'🏬 '}${esc(r.distributor)}</div>`:'';
+ return `<div class="qi" data-sku="${esc(r.sku)}">${img}
+ <div class="qm"><div class="qt" title="${esc(r.title||'')}">${esc(r.title||r.sku)}</div>
+ <div class="qs">${esc(r.sku)}</div>${d}</div>
+ <button class="qx" title="Remove from queue" onclick="removeFromQueue('${esc(r.sku)}')">✕</button></div>`;
+ }).join('');
+}
+async function removeFromQueue(sku){
+ try{await fetch(location.origin+'/api/import/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sku})});}catch(e){}
+ QUEUED.delete(sku); updateQueueBadge(); syncCardImp(sku,false);
+ const row=$('#qp-list').querySelector(`.qi[data-sku="${(window.CSS&&CSS.escape)?CSS.escape(sku):sku}"]`);
+ if(row)row.remove();
+ $('#qp-count').textContent=QUEUED.size.toLocaleString();
+ if(!QUEUED.size)renderQueue([]);
+}
+window.removeFromQueue=removeFromQueue;
+async function clearQueue(){
+ if(!QUEUED.size)return;
+ if(!confirm(`Clear all ${QUEUED.size.toLocaleString()} queued SKUs? (nothing is imported — this just empties the queue)`))return;
+ try{await fetch(location.origin+'/api/import/clear',{method:'POST'});}catch(e){}
+ const was=[...QUEUED]; QUEUED=new Set(); updateQueueBadge();
+ was.forEach(s=>syncCardImp(s,false)); renderQueue([]);
+}
+$('#qbadge').onclick=openQueue;
+$('#qp-close').onclick=()=>{qpanel().hidden=true;};
+$('#qp-clear').onclick=clearQueue;
async function toggleImport(sku,el){
const on=QUEUED.has(sku);
try{
@@ -321,6 +397,7 @@ async function toggleImport(sku,el){
if(on)QUEUED.delete(sku); else QUEUED.add(sku);
if(el){el.classList.toggle('on',!on);el.textContent=on?'⤓ import':'✓ queued';}
updateQueueBadge();
+ if(!$('#qpanel').hidden)openQueue();
}catch(e){}
}
window.toggleImport=toggleImport;
@@ -332,6 +409,7 @@ async function importFiltered(){
await fetch(location.origin+'/api/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({skus})});
const q=await fetch(location.origin+'/api/import-queue').then(r=>r.json());
QUEUED=new Set(q.skus||[]); updateQueueBadge(); applyFilters();
+ if(!$('#qpanel').hidden)openQueue();
}catch(e){}
}
$('#impfiltered').onclick=importFiltered;
diff --git a/viewer-local/server.js b/viewer-local/server.js
index 09b404b..53173c9 100644
--- a/viewer-local/server.js
+++ b/viewer-local/server.js
@@ -428,7 +428,11 @@ const server = http.createServer(async (req, res) => {
// ── import queue: enqueue INTENT for onboarding (reversible; no prod write) ──
if (u.pathname === '/api/import-queue') {
const q = readQueue();
- return sendJson(req, res, { count: q.length, skus: q.map((r) => r.sku) });
+ return sendJson(req, res, { count: q.length, skus: q.map((r) => r.sku), items: q });
+ }
+ if (u.pathname === '/api/import/clear' && req.method === 'POST') {
+ writeQueue([]);
+ return sendJson(req, res, { count: 0, cleared: true });
}
if (u.pathname === '/api/import' && req.method === 'POST') {
const body = await readBody(req);
← ab44253 japan viewer: '⤓ Import' button — enqueue SKUs for dw_unifie
·
back to Japan Enrich
·
japan: create per-vendor *_catalog tables (promote-queue tar af6d4f1 →