diff --git a/frontend/sortof-app.jsx b/frontend/sortof-app.jsx index 09aa68e..6827275 100644 --- a/frontend/sortof-app.jsx +++ b/frontend/sortof-app.jsx @@ -145,67 +145,47 @@ function WsidLink({ wsid, children, className }) { ); } -// Snapshot the fields needed to diff against the next sort/resort. Called -// before each fetch fires so the snapshot captures "what the user saw -// before this action". Polling-mid-flight ticks intentionally don't snapshot -// (would erase the prior visible state on every 2.5s update). -function snapshotForDiff(src) { - if (!src) return null; - return { - SORTED_ORDER: [...(src.SORTED_ORDER || [])], - MOD_DB: (src.MOD_DB || []).map(m => ({ modId: m.modId, wsid: m.wsid, name: m.name })), - MODS_LINE: src.MODS_LINE || '', - WORKSHOP_ITEMS_LINE: src.WORKSHOP_ITEMS_LINE || '', - }; -} - -function computeDiff(prev, curr) { - if (!prev || !curr) return null; - const prevSorted = prev.SORTED_ORDER || []; - const currSorted = curr.SORTED_ORDER || []; - const prevSet = new Set(prevSorted); - const currSet = new Set(currSorted); - const added = currSorted.filter(id => !prevSet.has(id)); - const removed = prevSorted.filter(id => !currSet.has(id)); - const prevPos = new Map(prevSorted.map((id, i) => [id, i])); +// Compare the wsids the user pasted (input order, deduped) against the wsids +// in WORKSHOP_ITEMS_LINE (sort order). Always available — no "previous sort" +// required. Surfaces drops (banned / missing mod.info / unknown / collection +// IDs that expanded), additions (collection expansion, branch picks), and +// position changes. +function computeDiff(inputWsids, outputWsids) { + if (!inputWsids || !outputWsids) return null; + const inSet = new Set(inputWsids); + const outSet = new Set(outputWsids); + const added = outputWsids.filter(w => !inSet.has(w)); + const removed = inputWsids.filter(w => !outSet.has(w)); + const inPos = new Map(inputWsids.map((w, i) => [w, i])); const movers = []; - currSorted.forEach((id, ci) => { - if (!prevSet.has(id)) return; - const pi = prevPos.get(id); - if (pi !== ci) movers.push({ id, from: pi, to: ci, delta: ci - pi }); + outputWsids.forEach((w, oi) => { + if (!inSet.has(w)) return; + const ii = inPos.get(w); + if (ii !== oi) movers.push({ wsid: w, from: ii, to: oi, delta: oi - ii }); }); movers.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta)); - const prevWsl = (prev.WORKSHOP_ITEMS_LINE || '').replace(/;+$/, '').split(';').filter(Boolean); - const currWsl = (curr.WORKSHOP_ITEMS_LINE || '').replace(/;+$/, '').split(';').filter(Boolean); - const prevWsidPos = new Map(prevWsl.map((w, i) => [w, i])); - const wsidMovers = []; - currWsl.forEach((w, ci) => { - const pi = prevWsidPos.get(w); - if (pi !== undefined && pi !== ci) wsidMovers.push({ wsid: w, from: pi, to: ci }); - }); - wsidMovers.sort((a, b) => Math.abs(b.to - b.from) - Math.abs(a.to - a.from)); - return { added, removed, movers, wsidMovers }; + return { added, removed, movers }; } -function DiffPanel({ prev, curr, onClose }) { - const diff = computeDiff(prev, curr); +function DiffPanel({ inputWsids, outputWsids, onClose }) { + const diff = computeDiff(inputWsids, outputWsids); if (!diff) { return (
- diff vs previous sort + diff: input → sorted
-
no previous sort to compare against - sort once first.
+
no input or output yet.
); } - const { added, removed, movers, wsidMovers } = diff; - const empty = !added.length && !removed.length && !movers.length && !wsidMovers.length; + const { added, removed, movers } = diff; + const empty = !added.length && !removed.length && !movers.length; return (
- diff vs previous sort + diff: input → sorted +{added.length} −{removed.length} @@ -213,41 +193,29 @@ function DiffPanel({ prev, curr, onClose }) {
- {empty &&
nothing changed.
} + {empty &&
order matches your input. nothing dropped or added.
} {added.length > 0 && (
-
added ({added.length})
- {added.slice(0, 30).map(id =>
+ {id}
)} +
added ({added.length}) — collection expansion or branch-picker
+ {added.slice(0, 30).map(w =>
+ {w}
)} {added.length > 30 &&
…and {added.length - 30} more
}
)} {removed.length > 0 && (
-
removed ({removed.length})
- {removed.slice(0, 30).map(id =>
− {id}
)} +
removed ({removed.length}) — dropped, banned, missing mod.info, or a collection ID that expanded
+ {removed.slice(0, 30).map(w =>
− {w}
)} {removed.length > 30 &&
…and {removed.length - 30} more
}
)} {movers.length > 0 && (
-
moved by load order ({movers.length}, top {Math.min(10, movers.length)} shown)
- {movers.slice(0, 10).map(({ id, from, to, delta }) => ( -
- {delta < 0 ? '↑' : '↓'} - {id} - pos {from + 1} → {to + 1} ({delta > 0 ? '+' : ''}{delta}) -
- ))} -
- )} - {wsidMovers.length > 0 && ( -
-
WorkshopItems= reorder ({wsidMovers.length})
- {wsidMovers.slice(0, 10).map(({ wsid, from, to }) => ( +
reordered by load order ({movers.length}, top {Math.min(10, movers.length)} shown)
+ {movers.slice(0, 10).map(({ wsid, from, to, delta }) => (
- {to < from ? '↑' : '↓'} + {delta < 0 ? '↑' : '↓'} {wsid} - pos {from + 1} → {to + 1} + pos {from + 1} → {to + 1} ({delta > 0 ? '+' : ''}{delta})
))}
@@ -1039,7 +1007,7 @@ function BuildToggle({ value, onChange }) { ); } -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 }) { +function RightColumn({ state, counts, progress, emptyVariant, successVariant, modTableDefault, pzBuild, setPzBuild, branchSelections, onToggleBranch, expandedWsids, onToggleExpansion, inputWsids, onAddWsid, onPickBranch, onSwapWsid, onRemoveWsid, onAutoFixAddDeps, onAutoFixSwaps, onAutoFixRemoves, onRetry, inputWsidList, 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'; @@ -1131,21 +1099,15 @@ function RightColumn({ state, counts, progress, emptyVariant, successVariant, mo type="button" className={'diff-toggle' + (diffOpen ? ' open' : '')} onClick={() => setDiffOpen(o => !o)} - disabled={!previousResult} - title={previousResult ? 'compare to previous sort' : 'sort once first'} + title="diff your input against the sorted output" > {diffOpen ? '▾ diff' : '▸ diff'}
{diffOpen && ( setDiffOpen(false)} /> )} @@ -1434,15 +1396,16 @@ function App() { const pollAbortRef = useRef(null); const [activeJobId, setActiveJobId] = useState(null); const [expandedWsids, setExpandedWsids] = useState(() => new Set()); - // Diff: snapshot of the result that's about to be replaced; toggle for the - // panel. Snapshot is taken at the START of any sort/resort - polling-mid-flight - // ticks don't snapshot so the user's prior visible state stays available. - const previousResultRef = useRef(null); + // Diff toggle. The diff is input-vs-output (textarea wsids vs sorted + // WORKSHOP_ITEMS_LINE), recomputed live every render — no snapshot ref + // needed. Always available, even on the first sort. const [diffOpen, setDiffOpen] = useState(false); - // Set of wsids currently in the input textarea, used by warning rows to - // derive their staged state. Memoized off `input` so re-renders triggered - // by unrelated state changes don't churn the Set. - const inputWsids = useMemo(() => new Set(parseWorkshopInput(input)), [input]); + // Ordered list of wsids currently in the input textarea (deduped, first-seen + // order). The Set wrapper below is for warning rows that need O(1) `.has()` + // lookup when deriving their staged state. Both are memoized off `input` so + // re-renders triggered by unrelated state changes don't churn them. + const inputWsidList = useMemo(() => parseWorkshopInput(input), [input]); + const inputWsids = useMemo(() => new Set(inputWsidList), [inputWsidList]); // True when the user has staged edits since the last successful sort. Stays // false until the first sort completes (lastSortedInputRef starts null). const sortPending = lastSortedInputRef.current !== null && input !== lastSortedInputRef.current; @@ -1589,8 +1552,6 @@ function App() { const onRetry = () => onSort(); async function runResort(nextSelections) { - // Snapshot for the diff panel: capture state before this resort. - previousResultRef.current = snapshotForDiff(_liveSortData); // Compose the flat list of selected mod_ids from MOD_DB + nextSelections. // For wsids not in nextSelections, use the §4 default (all-ticked or // first-only depending on radio mode). For wsids with N=1, include the @@ -1794,9 +1755,6 @@ function App() { // event as arg 0 - reject anything that isn't a string and fall back // to the state input. const submitInput = typeof inputOverride === 'string' ? inputOverride : input; - // Snapshot the about-to-be-replaced result so the [diff] button can - // surface what changed. Skip if there's nothing meaningful yet. - previousResultRef.current = snapshotForDiff(_liveSortData); clearTimers(); setState('loading'); setProgress(15); @@ -2065,7 +2023,7 @@ function App() { onAutoFixSwaps={onAutoFixSwaps} onAutoFixRemoves={onAutoFixRemoves} onRetry={onRetry} - previousResult={previousResultRef.current} + inputWsidList={inputWsidList} diffOpen={diffOpen} setDiffOpen={setDiffOpen} />