@@ -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 ) {
i f ( ! 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 computeDif f( 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 , c i) => {
if ( ! prev Set. has ( id ) ) return ;
const p i = prev Pos. get ( id ) ;
if ( p i !== c i) movers . push ( { id , from : p i, to : c i, delta : c i - p i } ) ;
outputWsids . forEach ( ( w , o i) => {
if ( ! in Set. has ( w ) ) return ;
const i i = in Pos. get ( w ) ;
if ( i i !== o i) movers . push ( { ws id: w , from : i i, to : o i, delta : o i - i i } ) ;
} ) ;
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 (
< div className = "diff-panel" >
< div className = "diff-head" >
< span className = "diff-title" > diff vs previous sort < / span >
< span className = "diff-title" > diff : input → sorted < / span >
< button type = "button" className = "diff-close" onClick = { onClose } > close < / button >
< / div >
< div className = "diff-empty" > no previous sor t t o compare against - sort once firs t. < / div >
< div className = "diff-empty" > no inpu t or output ye t. < / div >
< / div >
) ;
}
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 (
< div className = "diff-panel" >
< div className = "diff-head" >
< span className = "diff-title" > diff vs previous sort < / span >
< span className = "diff-title" > diff : input → sorted < / span >
< span className = "diff-summary" >
< span className = "diff-stat add" > + { added . length } < / span >
< span className = "diff-stat rm" > − { removed . length } < / span >
@@ -213,41 +193,29 @@ function DiffPanel({ prev, curr, onClose }) {
< / span >
< button type = "button" className = "diff-close" onClick = { onClose } > close < / button >
< / div >
{ empty && < div className = "diff-empty" > nothing chang ed. < / div > }
{ empty && < div className = "diff-empty" > order matches your input . nothing dropped or add ed. < / div > }
{ added . length > 0 && (
< div className = "diff-section" >
< div className = "diff-label" > added ( { added . length } ) < / div >
{ added . slice ( 0 , 30 ) . map ( id => < div key = { id } className = "diff-row diff-add" > + { id } < / div > ) }
< div className = "diff-label" > added ( { added . length } ) — collection expansion or branch - picker < / div >
{ added . slice ( 0 , 30 ) . map ( w => < div key = { w } className = "diff-row diff-add" > + { w } < / div > ) }
{ added . length > 30 && < div className = "diff-more" > … and { added . length - 30 } more < / div > }
< / div >
) }
{ removed . length > 0 && (
< div className = "diff-section" >
< div className = "diff-label" > removed ( { removed . length } ) < / div >
{ removed . slice ( 0 , 30 ) . map ( id => < div key = { id } className = "diff-row diff-rm" > − { id } < / div > ) }
< div className = "diff-label" > removed ( { removed . length } ) — dropped , banned , missing mod . info , or a collection ID that expanded < / div >
{ removed . slice ( 0 , 30 ) . map ( w => < div key = { w } className = "diff-row diff-rm" > − { w } < / div > ) }
{ removed . length > 30 && < div className = "diff-more" > … and { removed . length - 30 } more < / div > }
< / div >
) }
{ movers . length > 0 && (
< div className = "diff-section" >
< div className = "diff-label" > mov ed by load order ( { movers . length } , top { Math . min ( 10 , movers . length ) } shown ) < / div >
{ movers . slice ( 0 , 10 ) . map ( ( { id , from , to , delta } ) => (
< div key = { id } className = "diff-row diff-mv" >
< span className = "diff-arrow" > { delta < 0 ? '↑' : '↓' } < / span >
{ id }
< span className = "diff-pos" > pos { from + 1 } → { to + 1 } ( { delta > 0 ? '+' : '' } { delta } ) < / span >
< / div >
) ) }
< / div >
) }
{ wsidMovers . length > 0 && (
< div className = "diff-section" >
< div className = "diff-label" > WorkshopItems = reorder ( { wsidMovers . length } ) < / div >
{ wsidMovers . slice ( 0 , 10 ) . map ( ( { wsid , from , to } ) => (
< div className = "diff-label" > reorder ed by load order ( { movers . length } , top { Math . min ( 10 , movers . length ) } shown ) < / div >
{ movers . slice ( 0 , 10 ) . map ( ( { ws id, from , to , delta } ) => (
< div key = { wsid } className = "diff-row diff-mv" >
< span className = "diff-arrow" > { to < from ? '↑' : '↓' } < / span >
< span className = "diff-arrow" > { delta < 0 ? '↑' : '↓' } < / span >
{ wsid }
< span className = "diff-pos" > pos { from + 1 } → { to + 1 } < / span >
< span className = "diff-pos" > pos { from + 1 } → { to + 1 } ( { delta > 0 ? '+' : '' } { delta } ) < / span >
< / div >
) ) }
< / div >
@@ -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 , previousResul t, 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 , inputWsidLis t, 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 '}
</button>
</div>
{diffOpen && (
<DiffPanel
prev={previousResul t}
curr={{
SORTED_ORDER: D.SORTED_ORDER || [],
MOD_DB: D.MOD_DB || [],
MODS_LINE: D.MODS_LINE || ' ',
WORKSHOP_ITEMS_LINE: D.WORKSHOP_ITEMS_LINE || ' ',
}}
inputWsids={inputWsidLis t}
outputWsids={(D.WORKSHOP_ITEMS_LINE || ' ').replace(/;+$/, ' ').split(' ; ').filter(Boolean)}
onClose={() => 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 ) ;
// Se t 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 lis t of wsids currently in the input textarea (deduped, first-seen
// or der). 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.curren t}
inputWsidList={inputWsidLis t}
diffOpen={diffOpen}
setDiffOpen={setDiffOpen}
/>