From 8cabe7c9ff20925a46b8b5e972c951599ce299e0 Mon Sep 17 00:00:00 2001 From: Renamr Scaffold Date: Sun, 30 Aug 2026 05:05:22 +0000 Subject: [PATCH] Fix naming scheme silently resetting when a restored scheme collides with a milestone default The Match/Fetch "showcase the next field" auto-upgrade heuristics in RenamePanel.tsx each detected "the user hasn't customized the naming scheme yet" via an exact string match against SAMPLE_EXPRESSION / REAL_FILE_EXPRESSION / MATCHED_EXPRESSION. That was safe before the naming-scheme persistence feature existed, since in-memory expression state could only reach one of those exact strings by progressing through the auto-upgrade chain itself. Once persistence restores an arbitrary saved string on mount, a user's real custom scheme can legitimately equal one of those constants (this project's own default sample scheme is textually identical to the fully-fetched milestone expression) - so loading files, matching, or fetching would silently reset a correctly restored scheme back to a "simpler" milestone the next time a checkpoint's condition happened to match, undoing the just-loaded persisted value. Added restoredCustomSchemeRef, set once a real non-blank scheme is restored from disk, and gated all three auto-upgrade checkpoints on it so the onboarding auto-simplify chain only ever fires for a genuinely fresh session with nothing persisted - never for a returning session whose scheme was explicitly restored, regardless of what that restored text happens to equal. Verified with a new Playwright script reproducing the exact collision (persisted scheme == SAMPLE_EXPRESSION), confirming the scheme now survives Load Files, Match, and Fetch Data intact; all prior naming-scheme/ match-fetch/hash-files verification suites and `npm run build` still pass clean. --- src/components/RenamePanel.tsx | 37 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/components/RenamePanel.tsx b/src/components/RenamePanel.tsx index fe9bef2..d580b28 100644 --- a/src/components/RenamePanel.tsx +++ b/src/components/RenamePanel.tsx @@ -572,6 +572,19 @@ export default function RenamePanel() { expressionRef.current = expression; }, [expression]); + // Set true when a real persisted scheme is restored from disk below. The three + // "showcase the next field" auto-upgrade checkpoints further down (addRealFiles, + // runMatching, runFetch) each compare expressionRef.current against an exact + // milestone-constant string to infer "the user hasn't customized past this point + // yet" - a heuristic that only holds for a fresh in-memory session. A restored + // scheme can legitimately equal one of those constants (e.g. a user whose saved + // scheme IS the sample/fetched expression), and without this guard the very next + // checkpoint would silently stomp their correctly-restored scheme back to a + // "simpler" milestone the moment files are loaded/matched/fetched. Once a real + // value has been restored, none of the onboarding auto-upgrades should ever fire + // again for this session, regardless of what the restored text happens to equal. + const restoredCustomSchemeRef = useRef(false); + // Restores the naming-scheme editor's text from the last time the app was open (see // `src-tauri/src/settings.rs`'s `last_naming_scheme`) - without this, the editor // always reset to SAMPLE_EXPRESSION on every launch, silently discarding whatever @@ -582,7 +595,10 @@ export default function RenamePanel() { useEffect(() => { invoke("get_last_naming_scheme") .then((saved) => { - if (saved && saved.trim()) setExpression(saved); + if (saved && saved.trim()) { + setExpression(saved); + restoredCustomSchemeRef.current = true; + } }) .catch(() => {}) .finally(() => setSchemeLoaded(true)); @@ -642,8 +658,9 @@ export default function RenamePanel() { if (entries.length === 0) return; // Only auto-switch the naming scheme the first time real files replace the demo, - // and only if the user hasn't already customized it. - if (expressionRef.current === SAMPLE_EXPRESSION) { + // and only if the user hasn't already customized it (including a scheme restored + // from a previous session - see restoredCustomSchemeRef above). + if (expressionRef.current === SAMPLE_EXPRESSION && !restoredCustomSchemeRef.current) { setExpression(REAL_FILE_EXPRESSION); } @@ -877,9 +894,11 @@ export default function RenamePanel() { // Showcase the newly-populated fields, but only the first time and only if the // user hasn't already customized the naming scheme away from the plain-filename - // default. Movies deliberately don't drive this auto-upgrade - a single "best - // expression" heuristic doesn't generalize across a batch mixing TV and movies. - if (expressionRef.current === REAL_FILE_EXPRESSION && matchedTv > 0) { + // default (including a scheme restored from a previous session - see + // restoredCustomSchemeRef above). Movies deliberately don't drive this + // auto-upgrade - a single "best expression" heuristic doesn't generalize across a + // batch mixing TV and movies. + if (expressionRef.current === REAL_FILE_EXPRESSION && matchedTv > 0 && !restoredCustomSchemeRef.current) { setExpression(MATCHED_EXPRESSION); } @@ -1340,8 +1359,10 @@ export default function RenamePanel() { ); // Showcase the episode title field, but only the first time and only if the user - // hasn't already customized the naming scheme away from the Match-set default. - if (expressionRef.current === MATCHED_EXPRESSION && found > 0) { + // hasn't already customized the naming scheme away from the Match-set default + // (including a scheme restored from a previous session - see + // restoredCustomSchemeRef above). + if (expressionRef.current === MATCHED_EXPRESSION && found > 0 && !restoredCustomSchemeRef.current) { setExpression(FETCHED_EXPRESSION); }