working on add/fix/remove buttons
This commit is contained in:
@@ -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] = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1054,14 @@ 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 && (
|
||||
<>
|
||||
<AutoFixBar
|
||||
items={D.WARNINGS}
|
||||
inputWsids={inputWsids}
|
||||
onAddDeps={onAutoFixAddDeps}
|
||||
onFixMismatches={onAutoFixSwaps}
|
||||
onRemoveIncorrect={onAutoFixRemoves}
|
||||
/>
|
||||
<Warnings
|
||||
items={D.WARNINGS}
|
||||
defaultOpen={isTerminalDone}
|
||||
@@ -968,6 +1072,7 @@ function RightColumn({ state, counts, progress, emptyVariant, successVariant, mo
|
||||
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;
|
||||
setInput(prev => {
|
||||
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);
|
||||
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;
|
||||
setInput(prev => {
|
||||
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);
|
||||
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;
|
||||
setInput(prev => {
|
||||
const f = String(from);
|
||||
const t = String(to);
|
||||
const existing = parseWorkshopInput(input);
|
||||
// Already-swapped state → reverse direction.
|
||||
const existing = parseWorkshopInput(prev);
|
||||
if (!existing.includes(f) && existing.includes(t)) {
|
||||
const newInput = (input || '').replace(
|
||||
new RegExp(`(?<=^|[\\s;,])${t}(?=$|[\\s;,])`, 'g'),
|
||||
f
|
||||
);
|
||||
setInput(newInput);
|
||||
return;
|
||||
// Reverse a previously-applied swap.
|
||||
return prev.replace(new RegExp(`(?<=^|[\\s;,])${t}(?=$|[\\s;,])`, 'g'), f);
|
||||
}
|
||||
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;
|
||||
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);
|
||||
}
|
||||
const newInput = (input || '').replace(
|
||||
new RegExp(`(?<=^|[\\s;,])${f}(?=$|[\\s;,])`, 'g'),
|
||||
t
|
||||
);
|
||||
setInput(newInput);
|
||||
}
|
||||
}
|
||||
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}
|
||||
|
||||
@@ -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] = [
|
||||
|
||||
Reference in New Issue
Block a user