Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions spx-gui/src/apps/xbuilder/pages/editor/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import EditorContextProvider from '@/components/editor/EditorContextProvider.vue
import ProjectEditor from '@/components/editor/ProjectEditor.vue'
import { CodeEditorProvider, loadMonaco } from '@/components/editor/spx-code-editor'
import { usePublishProject } from '@/components/project'
import { useTutorial } from '@/components/tutorials/tutorial'
import { EditingMode, type ILocalCache } from '@/components/editor/editing'
import { EditorState } from '@/components/editor/editor-state'
import { cloudHelpers } from '@/models/common/cloud'
Expand All @@ -84,6 +85,7 @@ const i18n = useI18n()
const { t } = i18n
const { isOnline } = useNetwork()
const m = useMessage()
const tutorial = useTutorial()

const confirmOpenTargetWithAnotherInCache = (targetName: string, cachedName: string): Promise<boolean> => {
return confirm({
Expand Down Expand Up @@ -213,17 +215,24 @@ onBeforeRouteLeave(async () => {
* If it is OK to leave, return true, otherwise return false.
*/
async function checkChangesNotToBeSaved(es: EditorState) {
if (tutorial.consumeSkipLeaveConfirm()) return true
const hasEdits = es.editing.mode === EditingMode.EffectFree && es.editing.dirty
if (!hasEdits) return true
const inTutorial = tutorial.currentCourse != null
return confirm({
title: t({
en: 'Leave editor',
zh: '离开编辑器'
}),
content: t({
en: `Project edits will not be saved if you leave now. Are you sure to leave?`,
zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?`
}),
content: inTutorial
? t({
en: `Tutorial edits will not be saved if you leave now. Are you sure to leave?`,
zh: `教程中的修改不会被保存,确认要离开吗?`
})
: t({
Comment thread
Ethanlita marked this conversation as resolved.
Outdated
en: `Project edits will not be saved if you leave now. Are you sure to leave?`,
zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?`
}),
cancelText: t({
en: 'Keep editing',
zh: '继续编辑'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ function handleCancel() {
emit('cancelled')
}

function handleBrowseTutorials() {
function handleBackToCourseSeries() {
emit('cancelled')
router.push('/tutorials')
props.tutorial.requestSkipLeaveConfirm()
router.push(`/course-series/${props.series.id}`)
Comment thread
Ethanlita marked this conversation as resolved.
Outdated
Comment thread
Ethanlita marked this conversation as resolved.
Outdated
}

const hasNextCourse = computed(() => {
Expand Down Expand Up @@ -96,8 +97,8 @@ const { fn: handleStartNextCourse } = useMessageHandle(
<div class="mt-2 text-base">{{ courseCompleteMessage }}</div>

<div class="mt-10 w-full flex flex-col gap-5">
<UIButton type="neutral" size="large" @click="handleBrowseTutorials">
{{ $t({ zh: '浏览所有课程', en: 'Browse all courses' }) }}
<UIButton type="neutral" size="large" @click="handleBackToCourseSeries">
{{ $t({ zh: '返回系列课程', en: 'Back to Series Courses' }) }}
Comment thread
Ethanlita marked this conversation as resolved.
Outdated
</UIButton>
<UIButton v-if="hasNextCourse" size="large" @click="handleStartNextCourse">
{{ $t({ zh: '学习下一个课程', en: 'Learn next course' }) }}
Expand Down
24 changes: 24 additions & 0 deletions spx-gui/src/components/tutorials/tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import { tutorialCourseAbandonDismissal, tutorialCourseAbandonPrediction } from

const tutorialKey: InjectionKey<Tutorial> = Symbol('tutorial')

// Max age (ms) for a `requestSkipLeaveConfirm` request to still suppress the editor's
// leave confirmation. Time-bound so a request that is never consumed cannot get "stuck":
// some navigations (e.g. editor -> editor route updates, or starting a course from a
// non-editor page) never reach the editor's `onBeforeRouteLeave`, and an un-expiring
// flag would otherwise suppress a later, genuine leave confirmation.
const skipLeaveConfirmTimeout = 1000

export function useTutorial() {
const tutorial = inject(tutorialKey)
if (tutorial == null) {
Expand Down Expand Up @@ -61,6 +68,19 @@ export class Tutorial {
this.abandonPredictionCountRef.value = 0
}

// Skip the editor's leave confirmation for the navigation that immediately follows an
// explicit, expected action (e.g. clicking "back to course series" in the success modal,
// or starting a course). Time-bound via `skipLeaveConfirmTimeout` so it cannot get stuck.
Comment thread
Ethanlita marked this conversation as resolved.
Outdated
private skipLeaveConfirmRequestedAt = 0
requestSkipLeaveConfirm() {
this.skipLeaveConfirmRequestedAt = Date.now()
}
consumeSkipLeaveConfirm() {
const skip = Date.now() - this.skipLeaveConfirmRequestedAt < skipLeaveConfirmTimeout
this.skipLeaveConfirmRequestedAt = 0
return skip
}

async startCourse(course: Course, series: CourseSeries): Promise<void> {
try {
this.copilot.endCurrentSession()
Expand All @@ -71,6 +91,10 @@ export class Tutorial {
const { entrypoint } = course

if (entrypoint) {
// 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.
this.requestSkipLeaveConfirm()
await this.router.push(entrypoint)
await until(this.isRouteLoaded)
await timeout(100) // Wait for detailed UI rendering
Expand Down