From 8deaf82eacdb9cd00e8015ca29d652d0966fb0fa Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Wed, 6 May 2026 05:10:19 +0000 Subject: [PATCH] working on add/fix/remove buttons --- api/mlos_sort.py | 15 +- data/rules/curated/rv_expansion.txt | 9 +- frontend/index.html | 53 ++++++ frontend/sortof-app.jsx | 276 ++++++++++++++++++++-------- worker/mlos_sort.py | 15 +- 5 files changed, 280 insertions(+), 88 deletions(-) diff --git a/api/mlos_sort.py b/api/mlos_sort.py index 0d8d15b..af5b8fe 100644 --- a/api/mlos_sort.py +++ b/api/mlos_sort.py @@ -41,16 +41,21 @@ PREORDER: Dict[str, int] = { # Project-specific forced order: tsarslib is a foundational lib that many # vehicle/map mods require, so it must precede them. AquatsarYachtClub + # AquatsarRVAddon + ProjectRVInterior42 form an authored cluster whose - # interior overlays only render correctly in this exact order. Slots 4-7 - # land them immediately after the management tools, before any category- - # sorted content. + # interior overlays only render correctly in this exact order, and + # RVInteriorExpansion + Part2 (wsids 3618427553 / 3622163276) chain off + # PROJECTRVInterior42 — they don't declare loadAfter in their own mod.info + # and their category="undefined" drifts them to the end of MODS_LINE under + # category sort, so PREORDER pins them adjacent. Slots 4-9 land the cluster + # immediately after the management tools, before any category-sorted content. "tsarslib": 4, "AquatsarYachtClubB42": 5, "AquatsarRVAddon": 6, "PROJECTRVInterior42": 7, + "RVInteriorExpansion": 8, + "RVInteriorExpansionPart2": 9, # damnlib (wsid 3171167894) — same nature as tsarslib: foundational lib - # consumed by many B42 mods. Slots after the Aquatsar block. - "damnlib": 8, + # consumed by many B42 mods. Slots after the Aquatsar/RV cluster. + "damnlib": 10, } RAW_CATEGORY_ORDER: List[str] = [ diff --git a/data/rules/curated/rv_expansion.txt b/data/rules/curated/rv_expansion.txt index b3f4b70..16a5905 100644 --- a/data/rules/curated/rv_expansion.txt +++ b/data/rules/curated/rv_expansion.txt @@ -11,9 +11,12 @@ ; ; Authored ordering: PROJECTRVInterior42 → RVInteriorExpansion → RVInteriorExpansionPart2. ; The expansion mods don't declare these loadAfter relationships in their own -; mod.info, so without these rules sortof has nothing to base the cluster -; ordering on (PROJECTRVInterior42 is in PREORDER slot 7 already; expansions -; just need to chain off of it). +; mod.info. The actual adjacent placement is now enforced by PREORDER slots +; 8/9 in mlos_sort.py — `loadAfter` only constrains topo order and lets +; category-sort drift the expansions to the end of MODS_LINE under +; "undefined". The rules below remain as documentation of the intended chain +; AND so the `rules-applied` warning still fires when either expansion wsid +; is in the user's input. [RVInteriorExpansion] loadAfter=PROJECTRVInterior42 diff --git a/frontend/index.html b/frontend/index.html index 4143486..3449ea2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1040,6 +1040,59 @@ color: var(--brand-ink); border-color: var(--error); } + /* Auto-fix bar: 3 batched-action buttons that sit ABOVE the warnings panel. + * Each variant mirrors the corresponding inline .warn-action palette so + * "add deps" reads as info-blue, "fix mismatched" as success-green, and + * "remove incorrect" as error-red. Stages actions like inline buttons; sort + * is NOT fired (consistent with the staged-warning UI). */ + .auto-fix-bar { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; + padding: 0; + } + .auto-fix-btn { + appearance: none; + border: 1px solid; + font-family: var(--mono); + font-size: 12.5px; + font-weight: 600; + padding: 7px 14px; + border-radius: var(--radius); + cursor: pointer; + transition: color .12s, border-color .12s, background .12s, transform .08s; + } + .auto-fix-btn:active { transform: translateY(1px); } + .auto-fix-btn.add { + border-color: var(--acc-blue); + background: var(--acc-blue-bg, rgba(70,140,220,0.12)); + color: var(--acc-blue); + } + .auto-fix-btn.add:hover { + background: var(--acc-blue); + color: var(--brand-ink); + } + .auto-fix-btn.swap { + border-color: var(--success, var(--acc-green)); + background: var(--success-bg, rgba(80,180,120,0.12)); + color: var(--success, var(--acc-green)); + } + .auto-fix-btn.swap:hover { + background: var(--success, var(--acc-green)); + color: var(--brand-ink); + } + .auto-fix-btn.remove { + border-color: var(--error); + background: transparent; + color: var(--error); + } + .auto-fix-btn.remove:hover { + background: var(--error); + color: var(--brand-ink); + border-color: var(--error); + } + /* Staged warning row: edit applied to input but not yet sorted. Subtle — * strike + dim — so the row stays readable but visibly "addressed". The * action button itself flips to the success palette via .warn-action.staged diff --git a/frontend/sortof-app.jsx b/frontend/sortof-app.jsx index a4ff0e2..09aa68e 100644 --- a/frontend/sortof-app.jsx +++ b/frontend/sortof-app.jsx @@ -35,6 +35,38 @@ function buildModsLine(ids, mode) { return ids.join(';') + ';'; } +// Pure helpers for input-textarea mutations. Used by both the per-warning +// toggle handlers (onAddWsid etc.) and the batch auto-fix handlers. Pure so +// they can be folded inside a functional setInput(prev => …) call without +// stale-closure races. +function _applyAppendWsid(input, wsid) { + const trimmed = (input || '').replace(/\s+$/, ''); + const sep = trimmed ? '\n' : ''; + return trimmed + sep + wsid; +} +function _applyStripWsid(input, wsid) { + return (input || '').replace( + new RegExp(`(?:^|(?<=[\\s;,]))${wsid}(?=$|[\\s;,])`, 'g'), + '' + ).replace(/[\s;,]{2,}/g, m => m.includes('\n') ? '\n' : m[0]).trim(); +} +function _applyEnsureAdded(input, wsid) { + const wid = String(wsid); + return parseWorkshopInput(input).includes(wid) ? input : _applyAppendWsid(input, wid); +} +function _applyEnsureRemoved(input, wsid) { + const w = String(wsid); + return parseWorkshopInput(input).includes(w) ? _applyStripWsid(input, w) : input; +} +function _applyEnsureSwapped(input, from, to) { + const f = String(from); + const t = String(to); + const existing = parseWorkshopInput(input); + if (!existing.includes(f)) return input; // nothing to swap from + if (existing.includes(t)) return _applyStripWsid(input, f); // target present, just drop from + return input.replace(new RegExp(`(?<=^|[\\s;,])${f}(?=$|[\\s;,])`, 'g'), t); +} + // Spec B+F: poll a job. Resolves on terminal phase or AbortSignal. const POLL_INTERVAL_MS = 2500; @@ -684,6 +716,70 @@ function WarnRow({ w, inputWsids, onAddWsid, onPickBranch, onSwapWsid, onRemoveW ); } +// Auto-fix bar: 3 batched-action buttons that sit ABOVE the warnings panel. +// Each button stages every eligible action of its type in a single pass; sort +// is NOT fired (consistent with the staged-warning UI). A warning lives in +// exactly one bucket: missing-dep → Add, build-mismatch w/ swap → Fix, +// build-mismatch w/o swap → Remove. +function AutoFixBar({ items, inputWsids, onAddDeps, onFixMismatches, onRemoveIncorrect }) { + if (!items || items.length === 0) return null; + + let addCount = 0, swapCount = 0, removeCount = 0; + for (const w of items) { + if (!Array.isArray(w.actions)) continue; + if (w.tag === 'missing') { + // Eligible if any add-wsid target isn't yet in input. + if (w.actions.some(a => a.type === 'add-wsid' && a.wsid && !inputWsids.has(String(a.wsid)))) { + addCount++; + } + } else if (w.tag === 'build-mismatch') { + const swap = w.actions.find(a => a.type === 'swap-wsid' && a.from && a.to); + if (swap) { + // Eligible if from is present and to is absent. + if (inputWsids.has(String(swap.from)) && !inputWsids.has(String(swap.to))) { + swapCount++; + } + } else { + const rem = w.actions.find(a => a.type === 'remove-wsid' && a.wsid); + if (rem && inputWsids.has(String(rem.wsid))) { + removeCount++; + } + } + } + } + + if (addCount === 0 && swapCount === 0 && removeCount === 0) return null; + + return ( +
+ {addCount > 0 && ( + + )} + {swapCount > 0 && ( + + )} + {removeCount > 0 && ( + + )} +
+ ); +} + function Warnings({ items, defaultOpen = true, inputWsids, onAddWsid, onPickBranch, onSwapWsid, onRemoveWsid, onToggleBranch }) { const [open, setOpen] = useState(defaultOpen); if (!items || items.length === 0) return null; @@ -943,7 +1039,7 @@ function BuildToggle({ value, onChange }) { ); } -function RightColumn({ state, counts, progress, emptyVariant, successVariant, modTableDefault, pzBuild, setPzBuild, branchSelections, onToggleBranch, expandedWsids, onToggleExpansion, inputWsids, onAddWsid, onPickBranch, onSwapWsid, onRemoveWsid, onRetry, previousResult, diffOpen, setDiffOpen }) { +function RightColumn({ state, counts, progress, emptyVariant, successVariant, modTableDefault, pzBuild, setPzBuild, branchSelections, onToggleBranch, expandedWsids, onToggleExpansion, inputWsids, onAddWsid, onPickBranch, onSwapWsid, onRemoveWsid, onAutoFixAddDeps, onAutoFixSwaps, onAutoFixRemoves, onRetry, previousResult, diffOpen, setDiffOpen }) { // Phase-state mapping: legacy `success` ≈ B+F `done`; legacy `partial` ≈ B+F `queued`/`draining`. const isTerminalDone = state === 'success' || state === 'done'; const isInflightPartial = state === 'partial' || state === 'queued' || state === 'draining'; @@ -958,16 +1054,25 @@ function RightColumn({ state, counts, progress, emptyVariant, successVariant, mo * apply to are cached, so truncating to slice(0,1) was hiding load- * order issues from users mid-poll. */} {showWarnings && ( - + <> + + + )} {/* Cold cache banner */} @@ -1373,80 +1478,98 @@ function App() { }; // Append a wsid to the input textarea. Idempotent toggle: a second click - // on the same [add wsid] button removes the wsid (i.e., undoes the stage). - // Sort is NOT fired here — the user collects edits, then clicks sort. - // Used by the missing-dep warning's [add modId] action button. + // on the same [add wsid] button removes the wsid (undoes the stage). + // Functional setInput so the auto-fix batch handlers can chain mutations + // without stale-closure races. Sort is NOT fired here. const onAddWsid = (wsid /*, modId */) => { if (!wsid) return; - const wid = String(wsid); - const existing = parseWorkshopInput(input); - if (existing.includes(wid)) { - const newInput = (input || '').replace( - new RegExp(`(?:^|(?<=[\\s;,]))${wid}(?=$|[\\s;,])`, 'g'), - '' - ).replace(/[\s;,]{2,}/g, m => m.includes('\n') ? '\n' : m[0]).trim(); - setInput(newInput); - return; - } - const trimmed = (input || '').replace(/\s+$/, ''); - const sep = trimmed ? '\n' : ''; - setInput(trimmed + sep + wid); + setInput(prev => { + const wid = String(wsid); + return parseWorkshopInput(prev).includes(wid) + ? _applyStripWsid(prev, wid) + : _applyAppendWsid(prev, wid); + }); }; - // Remove a wsid from the input textarea token-by-token (preserving the - // user's separators). Idempotent toggle: a second click on the same - // [✕ remove] button re-appends the wsid. No sort fired. + // Remove a wsid from the input. Idempotent toggle: a second click re-appends. const onRemoveWsid = (wsid) => { if (!wsid) return; - const w = String(wsid); - const existing = parseWorkshopInput(input); - if (!existing.includes(w)) { - const trimmed = (input || '').replace(/\s+$/, ''); - const sep = trimmed ? '\n' : ''; - setInput(trimmed + sep + w); - return; - } - const newInput = (input || '').replace( - new RegExp(`(?:^|(?<=[\\s;,]))${w}(?=$|[\\s;,])`, 'g'), - '' - ).replace(/[\s;,]{2,}/g, m => m.includes('\n') ? '\n' : m[0]).trim(); - setInput(newInput); + setInput(prev => { + const w = String(wsid); + return parseWorkshopInput(prev).includes(w) + ? _applyStripWsid(prev, w) + : _applyAppendWsid(prev, w); + }); }; - // Replace `from` with `to` in the input textarea. Idempotent toggle: if the - // input is already in the swapped state (from absent, to present), a second - // click reverses to the original (to → from). No sort fired. Used by the - // build-mismatch warning's [swap to ] button (e.g., tsarslib - // at 2392709985 ↔ 3402491515). Preserves the user's separators. + // Replace `from` with `to` in the input. Idempotent toggle: if input is + // already in the swapped state (from absent, to present), reverse it. const onSwapWsid = (from, to) => { if (!from || !to) return; - const f = String(from); - const t = String(to); - const existing = parseWorkshopInput(input); - // Already-swapped state → reverse direction. - if (!existing.includes(f) && existing.includes(t)) { - const newInput = (input || '').replace( - new RegExp(`(?<=^|[\\s;,])${t}(?=$|[\\s;,])`, 'g'), - f - ); - setInput(newInput); - return; - } - if (!existing.includes(f)) return; - if (existing.includes(t)) { - // Target already present: just remove `from`. Otherwise we'd dupe. - const newInput = (input || '').replace( - new RegExp(`(?:^|(?<=[\\s;,]))${f}(?=$|[\\s;,])`, 'g'), - '' - ).replace(/[\s;,]{2,}/g, m => m.includes('\n') ? '\n' : m[0]).trim(); - setInput(newInput); - return; - } - const newInput = (input || '').replace( - new RegExp(`(?<=^|[\\s;,])${f}(?=$|[\\s;,])`, 'g'), - t - ); - setInput(newInput); + setInput(prev => { + const f = String(from); + const t = String(to); + const existing = parseWorkshopInput(prev); + if (!existing.includes(f) && existing.includes(t)) { + // Reverse a previously-applied swap. + return prev.replace(new RegExp(`(?<=^|[\\s;,])${t}(?=$|[\\s;,])`, 'g'), f); + } + return _applyEnsureSwapped(prev, f, t); + }); + }; + + // Auto-fix: stage every `add-wsid` action on `missing` warnings whose target + // isn't yet in input. Single batched setInput so all adds land in one pass. + // Sort is NOT fired — pending-cue + strikethroughs show the staged edits. + const onAutoFixAddDeps = () => { + const warns = (_liveSortData && _liveSortData.WARNINGS) || []; + setInput(prev => { + let working = prev; + for (const w of warns) { + if (w.tag !== 'missing' || !Array.isArray(w.actions)) continue; + for (const a of w.actions) { + if (a.type === 'add-wsid' && a.wsid) { + working = _applyEnsureAdded(working, a.wsid); + } + } + } + return working; + }); + }; + + // Auto-fix: stage every `swap-wsid` action on `build-mismatch` warnings. + // One swap per warning (warnings ship at most one swap action). Skips swaps + // already in their applied state (from absent, to present). + const onAutoFixSwaps = () => { + const warns = (_liveSortData && _liveSortData.WARNINGS) || []; + setInput(prev => { + let working = prev; + for (const w of warns) { + if (w.tag !== 'build-mismatch' || !Array.isArray(w.actions)) continue; + const swap = w.actions.find(a => a.type === 'swap-wsid' && a.from && a.to); + if (!swap) continue; + working = _applyEnsureSwapped(working, swap.from, swap.to); + } + return working; + }); + }; + + // Auto-fix: stage `remove-wsid` only on `build-mismatch` warnings that have + // NO `swap-wsid` (i.e., no B42 successor exists — drop is the only option). + // Warnings with both swap+remove are handled by onAutoFixSwaps and skipped here. + const onAutoFixRemoves = () => { + const warns = (_liveSortData && _liveSortData.WARNINGS) || []; + setInput(prev => { + let working = prev; + for (const w of warns) { + if (w.tag !== 'build-mismatch' || !Array.isArray(w.actions)) continue; + if (w.actions.some(a => a.type === 'swap-wsid')) continue; // handled by Fix + const rem = w.actions.find(a => a.type === 'remove-wsid' && a.wsid); + if (!rem) continue; + working = _applyEnsureRemoved(working, rem.wsid); + } + return working; + }); }; // Branch-warning button: lock the wsid to the chosen mod_id and resort. @@ -1938,6 +2061,9 @@ function App() { onPickBranch={onPickBranch} onSwapWsid={onSwapWsid} onRemoveWsid={onRemoveWsid} + onAutoFixAddDeps={onAutoFixAddDeps} + onAutoFixSwaps={onAutoFixSwaps} + onAutoFixRemoves={onAutoFixRemoves} onRetry={onRetry} previousResult={previousResultRef.current} diffOpen={diffOpen} diff --git a/worker/mlos_sort.py b/worker/mlos_sort.py index b739105..b5b7d2b 100644 --- a/worker/mlos_sort.py +++ b/worker/mlos_sort.py @@ -41,16 +41,21 @@ PREORDER: Dict[str, int] = { # Project-specific forced order: tsarslib is a foundational lib that many # vehicle/map mods require, so it must precede them. AquatsarYachtClub + # AquatsarRVAddon + ProjectRVInterior42 form an authored cluster whose - # interior overlays only render correctly in this exact order. Slots 4-7 - # land them immediately after the management tools, before any category- - # sorted content. + # interior overlays only render correctly in this exact order, and + # RVInteriorExpansion + Part2 (wsids 3618427553 / 3622163276) chain off + # PROJECTRVInterior42 — they don't declare loadAfter in their own mod.info + # and their category="undefined" drifts them to the end of MODS_LINE under + # category sort, so PREORDER pins them adjacent. Slots 4-9 land the cluster + # immediately after the management tools, before any category-sorted content. "tsarslib": 4, "AquatsarYachtClubB42": 5, "AquatsarRVAddon": 6, "PROJECTRVInterior42": 7, + "RVInteriorExpansion": 8, + "RVInteriorExpansionPart2": 9, # damnlib (wsid 3171167894) — same nature as tsarslib: foundational lib - # consumed by many B42 mods. Slots after the Aquatsar block. - "damnlib": 8, + # consumed by many B42 mods. Slots after the Aquatsar/RV cluster. + "damnlib": 10, } RAW_CATEGORY_ORDER: List[str] = [