mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
feat: changes to linker
This commit is contained in:
parent
25433757be
commit
3bcccb6a46
3 changed files with 265 additions and 132 deletions
|
|
@ -10,8 +10,19 @@ import {
|
|||
import { loadIndex } from "./state.js";
|
||||
import { aggregateBySku } from "./catalog.js";
|
||||
import { loadSkuRules, clearSkuRulesCache } from "./mapping.js";
|
||||
import { inferGithubOwnerRepo, isLocalWriteMode, loadSkuMetaBestEffort, apiWriteSkuLink, apiWriteSkuIgnore } from "./api.js";
|
||||
import { addPendingLink, addPendingIgnore, pendingCounts, loadPendingEdits } from "./pending.js";
|
||||
import {
|
||||
inferGithubOwnerRepo,
|
||||
isLocalWriteMode,
|
||||
loadSkuMetaBestEffort,
|
||||
apiWriteSkuLink,
|
||||
apiWriteSkuIgnore,
|
||||
} from "./api.js";
|
||||
import {
|
||||
addPendingLink,
|
||||
addPendingIgnore,
|
||||
pendingCounts,
|
||||
movePendingToSubmitted,
|
||||
} from "./pending.js";
|
||||
|
||||
/* ---------------- Similarity helpers ---------------- */
|
||||
|
||||
|
|
@ -93,7 +104,10 @@ function fastSimilarityScore(aTokens, bTokens, aNormName, bNormName) {
|
|||
const a = String(aNormName || "");
|
||||
const b = String(bNormName || "");
|
||||
const pref =
|
||||
firstMatch && a.slice(0, 10) && b.slice(0, 10) && a.slice(0, 10) === b.slice(0, 10)
|
||||
firstMatch &&
|
||||
a.slice(0, 10) &&
|
||||
b.slice(0, 10) &&
|
||||
a.slice(0, 10) === b.slice(0, 10)
|
||||
? 0.2
|
||||
: 0;
|
||||
|
||||
|
|
@ -229,8 +243,16 @@ function topSuggestions(allAgg, limit, otherPinnedSku, mappedSkus) {
|
|||
return scored.slice(0, limit).map((x) => x.it);
|
||||
}
|
||||
|
||||
function recommendSimilar(allAgg, pinned, limit, otherPinnedSku, mappedSkus, isIgnoredPairFn) {
|
||||
if (!pinned || !pinned.name) return topSuggestions(allAgg, limit, otherPinnedSku, mappedSkus);
|
||||
function recommendSimilar(
|
||||
allAgg,
|
||||
pinned,
|
||||
limit,
|
||||
otherPinnedSku,
|
||||
mappedSkus,
|
||||
isIgnoredPairFn
|
||||
) {
|
||||
if (!pinned || !pinned.name)
|
||||
return topSuggestions(allAgg, limit, otherPinnedSku, mappedSkus);
|
||||
|
||||
const base = String(pinned.name || "");
|
||||
const pinnedSku = String(pinned.sku || "");
|
||||
|
|
@ -243,7 +265,10 @@ function recommendSimilar(allAgg, pinned, limit, otherPinnedSku, mappedSkus, isI
|
|||
if (otherPinnedSku && String(it.sku) === String(otherPinnedSku)) continue;
|
||||
if (storesOverlap(pinned, it)) continue;
|
||||
|
||||
if (typeof isIgnoredPairFn === "function" && isIgnoredPairFn(pinnedSku, String(it.sku || "")))
|
||||
if (
|
||||
typeof isIgnoredPairFn === "function" &&
|
||||
isIgnoredPairFn(pinnedSku, String(it.sku || ""))
|
||||
)
|
||||
continue;
|
||||
|
||||
const s = similarityScore(base, it.name || "");
|
||||
|
|
@ -276,7 +301,9 @@ function computeInitialPairsFast(allAgg, mappedSkus, limitPairs, isIgnoredPairFn
|
|||
const itemNormName = new Map();
|
||||
|
||||
for (const it of work) {
|
||||
const toks = Array.from(new Set(tokenizeQuery(it.name || ""))).filter(Boolean).slice(0, 10);
|
||||
const toks = Array.from(new Set(tokenizeQuery(it.name || "")))
|
||||
.filter(Boolean)
|
||||
.slice(0, 10);
|
||||
itemTokens.set(it.sku, toks);
|
||||
itemNormName.set(it.sku, normSearchText(it.name || ""));
|
||||
for (const t of toks) {
|
||||
|
|
@ -307,7 +334,8 @@ function computeInitialPairsFast(allAgg, mappedSkus, limitPairs, isIgnoredPairFn
|
|||
if (!bSku || bSku === aSku) continue;
|
||||
if (mappedSkus && mappedSkus.has(bSku)) continue;
|
||||
|
||||
if (typeof isIgnoredPairFn === "function" && isIgnoredPairFn(aSku, bSku)) continue;
|
||||
if (typeof isIgnoredPairFn === "function" && isIgnoredPairFn(aSku, bSku))
|
||||
continue;
|
||||
if (storesOverlap(a, b)) continue;
|
||||
|
||||
cand.set(bSku, b);
|
||||
|
|
@ -378,7 +406,11 @@ export async function renderSkuLinker($app) {
|
|||
<button id="back" class="btn">← Back</button>
|
||||
<div style="flex:1"></div>
|
||||
<span class="badge">SKU Linker</span>
|
||||
${localWrite ? `<span class="badge mono">LOCAL WRITE</span>` : `<button id="createPrBtn" class="btn" disabled>Create PR</button>`}
|
||||
${
|
||||
localWrite
|
||||
? `<span class="badge mono">LOCAL WRITE</span>`
|
||||
: `<button id="createPrBtn" class="btn" disabled>Create PR</button>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:14px;">
|
||||
|
|
@ -512,14 +544,15 @@ export async function renderSkuLinker($app) {
|
|||
const oSku = String(otherPinned.sku || "");
|
||||
out = out.filter((it) => !isIgnoredPair(oSku, String(it.sku || "")));
|
||||
out = out.filter((it) => !storesOverlap(otherPinned, it));
|
||||
out = out.filter((it) => !sameGroup(oSku, String(it.sku || ""))); // <-- KEY FIX
|
||||
out = out.filter((it) => !sameGroup(oSku, String(it.sku || "")));
|
||||
}
|
||||
|
||||
return out.slice(0, 80);
|
||||
}
|
||||
|
||||
// auto-suggestions: never include mapped skus
|
||||
if (otherPinned) return recommendSimilar(allAgg, otherPinned, 60, otherSku, mappedSkus, isIgnoredPair);
|
||||
if (otherPinned)
|
||||
return recommendSimilar(allAgg, otherPinned, 60, otherSku, mappedSkus, isIgnoredPair);
|
||||
|
||||
if (initialPairs && initialPairs.length) {
|
||||
const list = side === "L" ? initialPairs.map((p) => p.a) : initialPairs.map((p) => p.b);
|
||||
|
|
@ -548,7 +581,6 @@ export async function renderSkuLinker($app) {
|
|||
return;
|
||||
}
|
||||
|
||||
// NEW: prevent pinning something in the same already-linked group as the other pinned item
|
||||
if (other && sameGroup(String(other.sku || ""), String(it.sku || ""))) {
|
||||
$status.textContent = "Already linked: both SKUs are in the same group.";
|
||||
return;
|
||||
|
|
@ -584,7 +616,6 @@ export async function renderSkuLinker($app) {
|
|||
function updateButtons() {
|
||||
const isPages = !localWrite;
|
||||
|
||||
// Pages: keep Create PR button state in sync with pending edits
|
||||
const $pr = isPages ? document.getElementById("createPrBtn") : null;
|
||||
if ($pr) {
|
||||
const c0 = pendingCounts();
|
||||
|
|
@ -639,31 +670,35 @@ export async function renderSkuLinker($app) {
|
|||
$status.textContent = "";
|
||||
}
|
||||
|
||||
// Refresh PR button state after any status changes
|
||||
if ($pr) {
|
||||
const c = pendingCounts();
|
||||
$pr.disabled = c.total === 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const $createPrBtn = document.getElementById("createPrBtn");
|
||||
if ($createPrBtn) {
|
||||
$createPrBtn.addEventListener("click", () => {
|
||||
$createPrBtn.addEventListener("click", async () => {
|
||||
const c = pendingCounts();
|
||||
if (c.total === 0) return;
|
||||
|
||||
const { owner, repo } = inferGithubOwnerRepo();
|
||||
const edits = loadPendingEdits();
|
||||
|
||||
// Payload is small (ops list), action will merge into data/sku_links.json
|
||||
// Move PENDING -> SUBMITTED (so it won't be sent again, but still affects suggestions/grouping)
|
||||
const editsToSend = movePendingToSubmitted();
|
||||
|
||||
const payload = JSON.stringify(
|
||||
{ schema: "stviz-sku-edits-v1", createdAt: edits.createdAt || new Date().toISOString(), links: edits.links, ignores: edits.ignores },
|
||||
{
|
||||
schema: "stviz-sku-edits-v1",
|
||||
createdAt: editsToSend.createdAt || new Date().toISOString(),
|
||||
links: editsToSend.links,
|
||||
ignores: editsToSend.ignores,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
const title = `[stviz] sku link updates (${c.links} link, ${c.ignores} ignore)`;
|
||||
const title = `[stviz] sku link updates (${editsToSend.links.length} link, ${editsToSend.ignores.length} ignore)`;
|
||||
const body =
|
||||
`Automated request from GitHub Pages SKU Linker.\n\n` +
|
||||
`<!-- stviz-sku-edits:BEGIN -->\n` +
|
||||
|
|
@ -675,17 +710,34 @@ export async function renderSkuLinker($app) {
|
|||
`/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
||||
|
||||
window.open(u, "_blank", "noopener,noreferrer");
|
||||
|
||||
// Refresh local rules so UI immediately reflects submitted shadow
|
||||
clearSkuRulesCache();
|
||||
rules = await loadSkuRules();
|
||||
ignoreSet = rules.ignoreSet;
|
||||
|
||||
const rebuilt = buildMappedSkuSet(rules.links || []);
|
||||
mappedSkus.clear();
|
||||
for (const x of rebuilt) mappedSkus.add(x);
|
||||
|
||||
const c2 = pendingCounts();
|
||||
$createPrBtn.disabled = c2.total === 0;
|
||||
|
||||
$status.textContent = "PR request opened. Staged edits moved to submitted (won’t re-suggest).";
|
||||
pinnedL = null;
|
||||
pinnedR = null;
|
||||
updateAll();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateAll() {
|
||||
renderSide("L");
|
||||
renderSide("R");
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
let tL = null, tR = null;
|
||||
let tL = null,
|
||||
tR = null;
|
||||
$qL.addEventListener("input", () => {
|
||||
if (tL) clearTimeout(tL);
|
||||
tL = setTimeout(() => {
|
||||
|
|
@ -760,7 +812,6 @@ export async function renderSkuLinker($app) {
|
|||
uniq.push(w);
|
||||
}
|
||||
|
||||
// ---------------- GitHub Pages mode: stage edits locally ----------------
|
||||
if (!localWrite) {
|
||||
for (const w of uniq) addPendingLink(w.fromSku, w.toSku);
|
||||
|
||||
|
|
@ -768,7 +819,6 @@ export async function renderSkuLinker($app) {
|
|||
rules = await loadSkuRules();
|
||||
ignoreSet = rules.ignoreSet;
|
||||
|
||||
// rebuild mappedSkus based on merged rules (includes pending)
|
||||
const rebuilt = buildMappedSkuSet(rules.links || []);
|
||||
mappedSkus.clear();
|
||||
for (const x of rebuilt) mappedSkus.add(x);
|
||||
|
|
@ -785,7 +835,6 @@ export async function renderSkuLinker($app) {
|
|||
return;
|
||||
}
|
||||
|
||||
// ---------------- Local mode: write via disk-backed API ----------------
|
||||
$status.textContent = `Writing ${uniq.length} link(s) to canonical ${displaySku(preferred)} …`;
|
||||
|
||||
try {
|
||||
|
|
@ -813,7 +862,6 @@ export async function renderSkuLinker($app) {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
$ignoreBtn.addEventListener("click", async () => {
|
||||
if (!(pinnedL && pinnedR)) return;
|
||||
|
||||
|
|
@ -841,7 +889,6 @@ export async function renderSkuLinker($app) {
|
|||
return;
|
||||
}
|
||||
|
||||
// ---------------- GitHub Pages mode: stage ignore locally ----------------
|
||||
if (!localWrite) {
|
||||
$status.textContent = `Staging ignore: ${displaySku(a)} × ${displaySku(b)} …`;
|
||||
|
||||
|
|
@ -851,6 +898,10 @@ export async function renderSkuLinker($app) {
|
|||
rules = await loadSkuRules();
|
||||
ignoreSet = rules.ignoreSet;
|
||||
|
||||
const rebuilt = buildMappedSkuSet(rules.links || []);
|
||||
mappedSkus.clear();
|
||||
for (const x of rebuilt) mappedSkus.add(x);
|
||||
|
||||
const c = pendingCounts();
|
||||
$status.textContent = `Staged locally. Pending: ${c.links} link(s), ${c.ignores} ignore(s).`;
|
||||
|
||||
|
|
@ -863,7 +914,6 @@ export async function renderSkuLinker($app) {
|
|||
return;
|
||||
}
|
||||
|
||||
// ---------------- Local mode: write via disk-backed API ----------------
|
||||
$status.textContent = `Ignoring: ${displaySku(a)} × ${displaySku(b)} …`;
|
||||
|
||||
try {
|
||||
|
|
@ -878,6 +928,5 @@ export async function renderSkuLinker($app) {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
updateAll();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,9 +171,12 @@ export async function loadSkuRules() {
|
|||
if (CACHED) return CACHED;
|
||||
|
||||
let meta = await loadSkuMetaBestEffort();
|
||||
|
||||
// On GitHub Pages (read-only), overlay local pending+submitted edits from localStorage
|
||||
if (!isLocalWriteMode()) {
|
||||
meta = applyPendingToMeta(meta);
|
||||
}
|
||||
|
||||
const links = Array.isArray(meta?.links) ? meta.links : [];
|
||||
const ignores = Array.isArray(meta?.ignores) ? meta.ignores : [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// viz/app/pending.js
|
||||
const LS_KEY = "stviz:v1:pendingSkuEdits";
|
||||
const LS_SUBMITTED_KEY = "stviz:v1:submittedSkuEdits";
|
||||
|
||||
function safeParseJson(s) {
|
||||
try {
|
||||
|
|
@ -9,10 +10,28 @@ function safeParseJson(s) {
|
|||
}
|
||||
}
|
||||
|
||||
export function loadPendingEdits() {
|
||||
function normSku(s) {
|
||||
return String(s || "").trim();
|
||||
}
|
||||
|
||||
function linkKey(fromSku, toSku) {
|
||||
const f = normSku(fromSku);
|
||||
const t = normSku(toSku);
|
||||
if (!f || !t || f === t) return "";
|
||||
return `${f}→${t}`;
|
||||
}
|
||||
|
||||
function pairKey(a, b) {
|
||||
const x = normSku(a);
|
||||
const y = normSku(b);
|
||||
if (!x || !y || x === y) return "";
|
||||
return x < y ? `${x}|${y}` : `${y}|${x}`;
|
||||
}
|
||||
|
||||
function loadEditsFromKey(key) {
|
||||
const raw = (() => {
|
||||
try {
|
||||
return localStorage.getItem(LS_KEY) || "";
|
||||
return localStorage.getItem(key) || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -23,82 +42,112 @@ export function loadPendingEdits() {
|
|||
const ignores = Array.isArray(j?.ignores) ? j.ignores : [];
|
||||
|
||||
return {
|
||||
links: links
|
||||
.map((x) => ({
|
||||
fromSku: String(x?.fromSku || "").trim(),
|
||||
toSku: String(x?.toSku || "").trim(),
|
||||
}))
|
||||
.filter((x) => x.fromSku && x.toSku && x.fromSku !== x.toSku),
|
||||
ignores: ignores
|
||||
.map((x) => ({
|
||||
skuA: String(x?.skuA || x?.a || "").trim(),
|
||||
skuB: String(x?.skuB || x?.b || "").trim(),
|
||||
}))
|
||||
.filter((x) => x.skuA && x.skuB && x.skuA !== x.skuB),
|
||||
createdAt: String(j?.createdAt || ""),
|
||||
links: links
|
||||
.map((x) => ({ fromSku: normSku(x?.fromSku), toSku: normSku(x?.toSku) }))
|
||||
.filter((x) => linkKey(x.fromSku, x.toSku)),
|
||||
ignores: ignores
|
||||
.map((x) => ({ skuA: normSku(x?.skuA || x?.a), skuB: normSku(x?.skuB || x?.b) }))
|
||||
.filter((x) => pairKey(x.skuA, x.skuB)),
|
||||
};
|
||||
}
|
||||
|
||||
export function savePendingEdits(edits) {
|
||||
function saveEditsToKey(key, edits) {
|
||||
const out = {
|
||||
createdAt: edits?.createdAt || new Date().toISOString(),
|
||||
links: Array.isArray(edits?.links) ? edits.links : [],
|
||||
ignores: Array.isArray(edits?.ignores) ? edits.ignores : [],
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(out));
|
||||
localStorage.setItem(key, JSON.stringify(out));
|
||||
} catch {}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function loadPendingEdits() {
|
||||
return loadEditsFromKey(LS_KEY);
|
||||
}
|
||||
|
||||
export function savePendingEdits(edits) {
|
||||
return saveEditsToKey(LS_KEY, edits);
|
||||
}
|
||||
|
||||
export function clearPendingEdits() {
|
||||
try {
|
||||
localStorage.removeItem(LS_KEY);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function canonicalPairKey(a, b) {
|
||||
const x = String(a || "").trim();
|
||||
const y = String(b || "").trim();
|
||||
if (!x || !y) return "";
|
||||
return x < y ? `${x}|${y}` : `${y}|${x}`;
|
||||
export function loadSubmittedEdits() {
|
||||
return loadEditsFromKey(LS_SUBMITTED_KEY);
|
||||
}
|
||||
|
||||
export function addPendingLink(fromSku, toSku) {
|
||||
const f = String(fromSku || "").trim();
|
||||
const t = String(toSku || "").trim();
|
||||
if (!f || !t || f === t) return false;
|
||||
|
||||
const edits = loadPendingEdits();
|
||||
const k = `${f}→${t}`;
|
||||
const seen = new Set(edits.links.map((x) => `${x.fromSku}→${x.toSku}`));
|
||||
if (seen.has(k)) return false;
|
||||
|
||||
edits.links.push({ fromSku: f, toSku: t });
|
||||
savePendingEdits(edits);
|
||||
return true;
|
||||
export function saveSubmittedEdits(edits) {
|
||||
return saveEditsToKey(LS_SUBMITTED_KEY, edits);
|
||||
}
|
||||
|
||||
export function addPendingIgnore(skuA, skuB) {
|
||||
const a = String(skuA || "").trim();
|
||||
const b = String(skuB || "").trim();
|
||||
if (!a || !b || a === b) return false;
|
||||
|
||||
const edits = loadPendingEdits();
|
||||
const k = canonicalPairKey(a, b);
|
||||
const seen = new Set(edits.ignores.map((x) => canonicalPairKey(x.skuA, x.skuB)));
|
||||
if (seen.has(k)) return false;
|
||||
|
||||
edits.ignores.push({ skuA: a, skuB: b });
|
||||
savePendingEdits(edits);
|
||||
return true;
|
||||
export function clearSubmittedEdits() {
|
||||
try {
|
||||
localStorage.removeItem(LS_SUBMITTED_KEY);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function pendingCounts() {
|
||||
const e = loadPendingEdits();
|
||||
return { links: e.links.length, ignores: e.ignores.length, total: e.links.length + e.ignores.length };
|
||||
return {
|
||||
links: e.links.length,
|
||||
ignores: e.ignores.length,
|
||||
total: e.links.length + e.ignores.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function addPendingLink(fromSku, toSku) {
|
||||
const f = normSku(fromSku);
|
||||
const t = normSku(toSku);
|
||||
const k = linkKey(f, t);
|
||||
if (!k) return false;
|
||||
|
||||
const pending = loadPendingEdits();
|
||||
const submitted = loadSubmittedEdits();
|
||||
|
||||
const seen = new Set(
|
||||
[
|
||||
...pending.links.map((x) => linkKey(x.fromSku, x.toSku)),
|
||||
...submitted.links.map((x) => linkKey(x.fromSku, x.toSku)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
|
||||
if (seen.has(k)) return false;
|
||||
|
||||
pending.links.push({ fromSku: f, toSku: t });
|
||||
savePendingEdits(pending);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function addPendingIgnore(skuA, skuB) {
|
||||
const a = normSku(skuA);
|
||||
const b = normSku(skuB);
|
||||
const k = pairKey(a, b);
|
||||
if (!k) return false;
|
||||
|
||||
const pending = loadPendingEdits();
|
||||
const submitted = loadSubmittedEdits();
|
||||
|
||||
const seen = new Set(
|
||||
[
|
||||
...pending.ignores.map((x) => pairKey(x.skuA, x.skuB)),
|
||||
...submitted.ignores.map((x) => pairKey(x.skuA, x.skuB)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
|
||||
if (seen.has(k)) return false;
|
||||
|
||||
pending.ignores.push({ skuA: a, skuB: b });
|
||||
savePendingEdits(pending);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Merge PENDING + SUBMITTED into a meta object {links, ignores}
|
||||
export function applyPendingToMeta(meta) {
|
||||
const base = {
|
||||
generatedAt: String(meta?.generatedAt || ""),
|
||||
|
|
@ -106,33 +155,65 @@ export function applyPendingToMeta(meta) {
|
|||
ignores: Array.isArray(meta?.ignores) ? meta.ignores.slice() : [],
|
||||
};
|
||||
|
||||
const p = loadPendingEdits();
|
||||
const p0 = loadPendingEdits();
|
||||
const p1 = loadSubmittedEdits();
|
||||
const overlay = {
|
||||
links: [...(p0.links || []), ...(p1.links || [])],
|
||||
ignores: [...(p0.ignores || []), ...(p1.ignores || [])],
|
||||
};
|
||||
|
||||
// merge links (dedupe by from→to)
|
||||
const seenL = new Set(
|
||||
base.links.map((x) => `${String(x?.fromSku || "").trim()}→${String(x?.toSku || "").trim()}`)
|
||||
base.links.map((x) => linkKey(String(x?.fromSku || "").trim(), String(x?.toSku || "").trim())).filter(Boolean)
|
||||
);
|
||||
for (const x of p.links) {
|
||||
const k = `${x.fromSku}→${x.toSku}`;
|
||||
if (!seenL.has(k)) {
|
||||
for (const x of overlay.links) {
|
||||
const k = linkKey(x.fromSku, x.toSku);
|
||||
if (!k || seenL.has(k)) continue;
|
||||
seenL.add(k);
|
||||
base.links.push({ fromSku: x.fromSku, toSku: x.toSku });
|
||||
}
|
||||
}
|
||||
|
||||
// merge ignores (dedupe by canonical pair key)
|
||||
const seenI = new Set(
|
||||
base.ignores.map((x) =>
|
||||
canonicalPairKey(String(x?.skuA || x?.a || "").trim(), String(x?.skuB || x?.b || "").trim())
|
||||
)
|
||||
base.ignores
|
||||
.map((x) => pairKey(String(x?.skuA || x?.a || "").trim(), String(x?.skuB || x?.b || "").trim()))
|
||||
.filter(Boolean)
|
||||
);
|
||||
for (const x of p.ignores) {
|
||||
const k = canonicalPairKey(x.skuA, x.skuB);
|
||||
if (!seenI.has(k)) {
|
||||
for (const x of overlay.ignores) {
|
||||
const k = pairKey(x.skuA, x.skuB);
|
||||
if (!k || seenI.has(k)) continue;
|
||||
seenI.add(k);
|
||||
base.ignores.push({ skuA: x.skuA, skuB: x.skuB });
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
// Move everything from pending -> submitted, then clear pending.
|
||||
// Returns the moved payload (what should be sent in PR/issue).
|
||||
export function movePendingToSubmitted() {
|
||||
const pending = loadPendingEdits();
|
||||
if (!pending.links.length && !pending.ignores.length) return pending;
|
||||
|
||||
const sub = loadSubmittedEdits();
|
||||
|
||||
const seenL = new Set(sub.links.map((x) => linkKey(x.fromSku, x.toSku)).filter(Boolean));
|
||||
for (const x of pending.links) {
|
||||
const k = linkKey(x.fromSku, x.toSku);
|
||||
if (!k || seenL.has(k)) continue;
|
||||
seenL.add(k);
|
||||
sub.links.push({ fromSku: x.fromSku, toSku: x.toSku });
|
||||
}
|
||||
|
||||
const seenI = new Set(sub.ignores.map((x) => pairKey(x.skuA, x.skuB)).filter(Boolean));
|
||||
for (const x of pending.ignores) {
|
||||
const k = pairKey(x.skuA, x.skuB);
|
||||
if (!k || seenI.has(k)) continue;
|
||||
seenI.add(k);
|
||||
sub.ignores.push({ skuA: x.skuA, skuB: x.skuB });
|
||||
}
|
||||
|
||||
saveSubmittedEdits(sub);
|
||||
clearPendingEdits();
|
||||
return pending;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue