← back to Dw Collections Viewer
bulk product remove-from-list (server endpoint + UI button)
3f6faec63a142cbdf49d30c430c798861683c4e9 · 2026-05-06 13:03:43 -0700 · SteveStudio2
Files touched
M public/index.htmlM server.js
Diff
commit 3f6faec63a142cbdf49d30c430c798861683c4e9
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 6 13:03:43 2026 -0700
bulk product remove-from-list (server endpoint + UI button)
---
public/index.html | 17 +++++++++++++++++
server.js | 17 +++++++++++++++++
2 files changed, 34 insertions(+)
diff --git a/public/index.html b/public/index.html
index 5794923..0683a6b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -451,6 +451,7 @@ function renderProductsSlider() {
<div class="actions-mini">
${selCount ? `<button onclick="clearProductSelection()">Clear (${selCount})</button>` : ''}
${selCount ? `<button onclick="copySelectedSkus()">Copy SKUs</button>` : ''}
+ ${selCount ? `<button onclick="removeSelectedFromList()">Remove (${selCount})</button>` : ''}
<button onclick="selectAllProducts()">Select all</button>
</div>
</div>
@@ -472,6 +473,22 @@ function copySelectedSkus() {
const skus = Array.from(PRODUCT_SELECTED).join('\n');
navigator.clipboard.writeText(skus).then(() => toast(`copied ${PRODUCT_SELECTED.size} SKUs to clipboard`));
}
+async function removeSelectedFromList() {
+ const slug = CURRENT;
+ const skus = Array.from(PRODUCT_SELECTED);
+ if (!skus.length) return;
+ if (!confirm(`Remove ${skus.length} product(s) from ${slug}'s local products.json? Live site is unchanged until you redeploy.`)) return;
+ toast(`removing ${skus.length} from ${slug}…`);
+ const r = await fetch(`api/site/${slug}/products/remove`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ skus }),
+ });
+ const j = await r.json();
+ if (!r.ok) return toast('remove failed: ' + (j.error || r.status), false);
+ toast(`removed ${j.removed} · ${j.before} → ${j.after} (not deployed)`, true, j.removed > 0 ? 'down' : null);
+ PRODUCT_SELECTED.clear();
+ await fetchProducts(slug);
+}
function renderDetail() {
const s = SITES[CURRENT];
diff --git a/server.js b/server.js
index 980e43b..30f6c8d 100644
--- a/server.js
+++ b/server.js
@@ -416,6 +416,23 @@ app.get('/api/site/:slug/products', (req, res) => {
res.json({ slug, count: products.length, products: indexed.map(x => x.p) });
});
+// Remove SKUs from a site's local products.json (does NOT touch Shopify).
+// Body: { skus: ["SKU1", ...] }. Persists filtered products.json to disk.
+app.post('/api/site/:slug/products/remove', (req, res) => {
+ const { slug } = req.params;
+ if (!validSlug(slug)) return res.status(400).json({ error: 'invalid slug' });
+ const skus = Array.isArray(req.body.skus) ? req.body.skus.filter(s => typeof s === 'string' && s) : [];
+ if (!skus.length) return res.status(400).json({ error: 'skus[] required' });
+ const fp = path.join(HOME, 'Projects', slug, 'data', 'products.json');
+ const products = readJsonSafe(fp) || [];
+ const before = products.length;
+ const skuSet = new Set(skus);
+ const filtered = products.filter(p => !skuSet.has(String(p.sku || p.handle || '')));
+ const removed = before - filtered.length;
+ if (removed > 0) fs.writeFileSync(fp, JSON.stringify(filtered, null, 2));
+ res.json({ slug, removed, before, after: filtered.length });
+});
+
// ─── All-products view + bulk delete + bulk add-to-collection ───────────
// Read uses public /products.json (no token). Writes need Admin token in env.
const SHOPIFY_STORE = process.env.SHOPIFY_STORE || 'designerwallcoverings.com';
← 8a47889 press C anywhere to focus collection search input
·
back to Dw Collections Viewer
·
Click site → show all products as a responsive grid (was hor f016e8f →