feat(tutorial): refine editor-leave confirmation and course success modal navigation#3305
feat(tutorial): refine editor-leave confirmation and course success modal navigation#3305Ethanlita wants to merge 8 commits into
Conversation
…eries - Show tutorial-specific copy in the leave-editor confirm dialog when a course is active; keep the original copy otherwise - Make the course success modal's "back to course series" button navigate to the current course's series instead of /tutorials Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a one-shot skip flag on Tutorial so explicit, expected navigations away from the editor do not trigger the leave confirmation: - "Back to course series" in the success modal requests the skip before navigating to the series page - startCourse requests the skip before pushing the course entrypoint, which may point to a non-editor route Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to skip the editor's leave confirmation dialog during explicit, expected navigation actions (such as starting a new course or returning to the course series page) and updates the confirmation message to be tutorial-specific when in a tutorial. Feedback was provided regarding a potential bug where the skip-confirmation flag could get "stuck" as true if navigating between editor routes (which triggers onBeforeRouteUpdate instead of onBeforeRouteLeave). A time-bound expiration mechanism was suggested to resolve this issue.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
A plain one-shot flag could leak: navigations that don't reach the editor's onBeforeRouteLeave (e.g. editor -> editor route updates when starting the next course, or starting a course from a non-editor page) never consume it, so a later genuine leave would silently skip its confirmation. Expire the skip request after skipLeaveConfirmTimeout instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Summary
The time-bounded skip-flag design is solid — the comments clearly explain both what the mechanism does and why expiry is necessary. The correctness tradeoff (a skip request that is never consumed can suppress a genuine leave within its 1 s window) is explicitly acknowledged in the PR commit messages, which is good. A few issues worth addressing before merge:
Ordering risk in handleBackToCourseSeries — requestSkipLeaveConfirm() is called after emit('cancelled'). If the parent's cancelled handler takes any time, the 1 s window starts later than necessary. Moving requestSkipLeaveConfirm() to be the first call in the function minimises the gap and removes dependence on the parent handler's cost:
function handleBackToCourseSeries() {
props.tutorial.requestSkipLeaveConfirm() // first
emit('cancelled')
router.push(`/course-series/${props.series.id}`)
}Unhandled router.push rejection — router.push(...) returns a promise that can reject (navigation guard cancellation, NavigationDuplicated, etc.). handleStartNextCourse is already wrapped in useMessageHandle; it's worth adding a .catch or similar error handling to handleBackToCourseSeries as well, consistent with the surrounding code.
Button label capitalisation — 'Back to Series Courses' uses Title Case, while every other button in this file and neighbouring pages uses sentence case ('Learn next course', 'Back to apps', 'Back to users'). Should be 'Back to series courses'.
Comment/label mismatch — tutorial.ts line ~73 quotes the button as "back to course series", but the actual label is "Back to Series Courses" (word order differs too). Should match the rendered string.
zh copy inconsistency — The tutorial and project leave-confirm messages use different confirmation verbs: 确认要离开吗 vs 确定要离开吗. Aligning them avoids a subtle inconsistency in the UX.
- Request the leave-confirm skip before emit('cancelled') so the time-bound
window isn't shortened by the cancel handler
- Wrap "back to course series" navigation in useMessageHandle to surface
router.push rejections, consistent with handleStartNextCourse
- Use sentence case for the button label: "Back to series courses"
- Align the tutorial.ts comment with the actual button label
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Invert the dependency so the editor no longer needs tutorial knowledge. Add an editor-owned `editorLeaveConfirm` controller exposing a generic message override and a one-shot, time-bound skip. Tutorial drives it the same way it drives copilot (TutorialRoot sets the tutorial-specific copy while a course is active; startCourse and the success modal request the skip), and the editor page's leave guard only reads the controller. The editor stays unaware of tutorials (no components/editor -> tutorials import), and pages/editor no longer imports useTutorial. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| // A course entrypoint may point to a non-editor route. Navigating away from the | ||
| // editor would otherwise trigger its leave confirmation, but starting a course is | ||
| // an explicit, expected action, so skip the confirmation for this navigation. | ||
| editorLeaveConfirm.requestSkipOnce() |
There was a problem hiding this comment.
跳过 confirm 的逻辑放到 TutorialCourseSuccessModal.vue 中调用 startCourse 的地方做可能更合适?按我们讨论的结论,不是所有的 startCourse 都应该跳过 confirm 的,只是 TutorialCourseSuccessModal 中那个 startCourse 行为才应该跳过
| * If it is OK to leave, return true, otherwise return false. | ||
| */ | ||
| async function checkChangesNotToBeSaved(es: EditorState) { | ||
| if (editorLeaveConfirm.consumeSkipOnce()) return true |
There was a problem hiding this comment.
通过单独的机制来避免跟 editor 跟 tutorial 逻辑的耦合,这个思路我觉得没问题。不过现在这个 editor-leave-confirm 稍微有点重,我在想能不能借用已有的 editing-dirty 来达到这次的目的;比如 class Editing 增加个方法来重置 dirty 状态,现在 requestSkipOnce 的地方通过重置 dirty 状态来跳过检查?
There was a problem hiding this comment.
两种方式都能达到目的,这里我倾向先保留独立的 editorLeaveConfirm:
语义更直白——它直接表达「从成功弹窗点按钮的这次导航跳过离开确认」这个意图;而用重置 dirty 来实现,是借副作用达成跳过,需要额外注释说明「此处重置 dirty 是为了跳过离开确认」才不至于让人误解。
There was a problem hiding this comment.
是的两种方式都能达到目的
做个类比的话,这里有点像是来了一个超市,拿了东西走,门口的保安会提醒结账。非 effect-free mode 下保存后离开 editor 就像是正常结账然后离开超市,reset 后离开就像是把手里东西放下然后离开,editor-leave-confirm 机制就像是为此让超市去发行一种特殊的 VIP 卡,拿着 VIP 卡就不会被拦,然后我们拿着东西拿着 VIP 卡离开。因为我们确实是要丢弃在当前 editor 中做的改动的,就像我们确实不需要带走手里的东西,所以把东西放下然后离开也是比较常规的思路。与其说它是“借助副作用达成跳过”,不如说是它是在遵从已有的规则来达到跳过的目的。
独立的 editor-leave-confirm 机制确实可以避免注释,但是代价是新增了一个机制。它避免注释的方式跟我们走 reset 逻辑但是封装一个类似
function skipConfirmForLeavingEditor() {
editing.resetChanges()
}的 function 没多大区别。当然这个函数也要比 editor-leave-confirm 机制的实现要更轻。
这个 editor-leave-confirm 机制如果要完善地实现,它要比现在代码中的更复杂一些;因为我们现在的做法是:借助 module export(editorLeaveConfirm 这个单例)来实现 tutorial 模块与 editor 模块的交互,不过在 UI 逻辑中更合理的做法是借助 Vue 的 provide / inject 机制来共享这份实例(达到通信的目的)。所以它要比现在的版本要更复杂些。
| // Drive the editor's leave confirmation with tutorial-specific copy while a course is | ||
| // active; the editor exposes the override and stays unaware of tutorials. | ||
| editorLeaveConfirm.setMessageOverride({ | ||
| en: 'Tutorial edits will not be saved if you leave now. Are you sure to leave?', |
There was a problem hiding this comment.
这个文案跟默认文案表达的信息差别并不大,有没有可能优化默认文案来让它在不同场景下都比较合适,而不是针对 tutorial 使用不一样的文案?
There was a problem hiding this comment.
这条离开确认的文案是 @wenjiangping 定的。@wenjiangping 麻烦看下能否把默认文案统一成一个在教程 / 非教程场景都合适的措辞,这样就不必为 tutorial 单独 override 了。
There was a problem hiding this comment.
我们设计这边也赞成使用统一的通用文案。期望更改成:”对项目的修改将不会被保存,确定要离开吗?“,与线上环境相比,主要去掉了前半句“若现在离开”的文案,因为“若现在离开”的表述容易产生“以后是否可以保存”的歧义。@Ethanlita
Only the success modal's course navigations leave the editor as an explicit, expected action; other startCourse callers (course-start / course-series pages) are not leaving an editor. Request the skip at the success modal call site instead of inside startCourse, so it no longer applies to every caller. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ride Use a single leave-confirmation message for all effect-free cases instead of a tutorial-specific one. The copy also drops the "if you leave now" clause, which could imply the edits are saveable later. editorLeaveConfirm now only exposes the one-shot skip, and the tutorial layer no longer drives the confirmation copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…state The tutorial success modal's "back to series" and "next course" actions intentionally discard the current effect-free edits, so reset the editing dirty state before navigating and the editor's leave confirmation is naturally skipped. Add Editing.resetChanges(); the success modal reaches the current editor through the existing editor context (useEditorCtxRef). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Introduce an editor-owned extension point for the "leave editor" confirmation and use it to refine the tutorial experience, so tutorial features can adjust the editor's leave behavior .
User-facing changes
/course-series/:id). ("Learn next course" is unchanged.)Implementation & design
components/editor/leave-confirm.ts(editorLeaveConfirm) — a generic extension point for the editor's leave confirmation:messageOverride— replace the confirmation copy.requestSkipOnce/consumeSkipOnce— skip the confirmation for a single, expected navigation. The skip is time-bound, so a request that is never consumed (e.g. aneditor → editorroute update, or starting a course from a non-editor page, neither of which reachesonBeforeRouteLeave) cannot get stuck and silently suppress a later, genuine leave.onBeforeRouteLeaveguard only readseditorLeaveConfirm. The tutorial layer drives it, the same way it drives copilot:TutorialRootsets the tutorial copy while a course is active (and clears it on cleanup), andstartCourse/ the success modal request the skip. There is nocomponents/editor → components/tutorialsdependency, andpages/editorno longer importsuseTutorial.Validation
npm run type-check,npm run lint, andediting.test.ts(22 tests) pass.