working on add/fix/remove buttons

This commit is contained in:
2026-05-06 05:10:19 +00:00
parent 158bc7c1d7
commit 8deaf82eac
5 changed files with 280 additions and 88 deletions

View File

@@ -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] = [

View File

@@ -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

View File

@@ -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

View File

@@ -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 (
<div className="auto-fix-bar" role="group" aria-label="auto-fix actions">
{addCount > 0 && (
<button
type="button"
className="auto-fix-btn add"
onClick={onAddDeps}
title="stage every missing dependency. click sort when ready."
>+ add {addCount} {addCount === 1 ? 'dependency' : 'dependencies'}</button>
)}
{swapCount > 0 && (
<button
type="button"
className="auto-fix-btn swap"
onClick={onFixMismatches}
title="stage every build-mismatch swap. click sort when ready."
> fix {swapCount} mismatched build{swapCount === 1 ? '' : 's'}</button>
)}
{removeCount > 0 && (
<button
type="button"
className="auto-fix-btn remove"
onClick={onRemoveIncorrect}
title="stage removal of build-mismatch mods with no B42 successor. click sort when ready."
> remove {removeCount} incorrect build{removeCount === 1 ? '' : 's'}</button>
)}
</div>
);
}
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 && (
<Warnings
items={D.WARNINGS}
defaultOpen={isTerminalDone}
inputWsids={inputWsids}
onAddWsid={onAddWsid}
onPickBranch={onPickBranch}
onSwapWsid={onSwapWsid}
onRemoveWsid={onRemoveWsid}
onToggleBranch={onToggleBranch}
/>
<>
<AutoFixBar
items={D.WARNINGS}
inputWsids={inputWsids}
onAddDeps={onAutoFixAddDeps}
onFixMismatches={onAutoFixSwaps}
onRemoveIncorrect={onAutoFixRemoves}
/>
<Warnings
items={D.WARNINGS}
defaultOpen={isTerminalDone}
inputWsids={inputWsids}
onAddWsid={onAddWsid}
onPickBranch={onPickBranch}
onSwapWsid={onSwapWsid}
onRemoveWsid={onRemoveWsid}
onToggleBranch={onToggleBranch}
/>
</>
)}
{/* 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 <other-build>] 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}

View File

@@ -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] = [