Skip to content
Open
4 changes: 2 additions & 2 deletions spx-gui/src/apps/xbuilder/pages/editor/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ async function checkChangesNotToBeSaved(es: EditorState) {
zh: '离开编辑器'
}),
content: t({
en: `Project edits will not be saved if you leave now. Are you sure to leave?`,
zh: `若现在离开,对项目的修改将不会被保存确定要离开吗?`
en: `Project edits will not be saved. Are you sure to leave?`,
zh: `对项目的修改将不会被保存确定要离开吗?`
}),
cancelText: t({
en: 'Keep editing',
Expand Down
11 changes: 11 additions & 0 deletions spx-gui/src/components/editor/editing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ export class Editing extends Disposable {
return this.dirtyRef.value
}

/**
* Mark the current unsaved changes as no longer pending, clearing the dirty state.
*
* This only resets dirty tracking; it does not revert the project content. It is used when
* the user intentionally leaves without keeping the edits (e.g. finishing a tutorial course
* in effect-free mode), so leaving the editor does not prompt the "leave editor" confirmation.
*/
resetChanges() {
this.dirtyRef.value = false
}

private startDirtyMonitoring() {
this.addDisposer(
watch(
Expand Down
28 changes: 22 additions & 6 deletions spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CourseSeries } from '@/apis/course-series'
import { useI18n } from '@/utils/i18n'

import { UIButton, UIImg, UIModal, UIModalClose } from '@/components/ui'
import { useEditorCtxRef } from '@/components/editor/EditorContextProvider.vue'
import { DefaultException, useMessageHandle } from '@/utils/exception'
import successImg from './success.png'

Expand All @@ -25,6 +26,13 @@ const emit = defineEmits<{

const i18n = useI18n()
const router = useRouter()
const editorCtxRef = useEditorCtxRef()

// Leaving via the actions below intentionally discards the current (effect-free) tutorial
// edits, so reset the dirty state to skip the editor's "leave editor" confirmation.
function skipConfirmForLeavingEditor() {
editorCtxRef.value?.state.editing.resetChanges()
}

const courseCompleteMessage = computed(() => {
return i18n.t({
Expand All @@ -37,10 +45,17 @@ function handleCancel() {
emit('cancelled')
}

function handleBrowseTutorials() {
emit('cancelled')
router.push('/tutorials')
}
const { fn: handleBackToCourseSeries } = useMessageHandle(
async () => {
skipConfirmForLeavingEditor()
emit('cancelled')
await router.push(`/course-series/${props.series.id}`)
},
{
en: 'Failed to go back to course series',
zh: '返回系列课程失败'
}
)

const hasNextCourse = computed(() => {
const currentCourse = props.course
Expand All @@ -64,6 +79,7 @@ const { fn: handleStartNextCourse } = useMessageHandle(
const tutorial = props.tutorial
const nextCourse = await getCourse(currentSeries.courseIDs[findIndex + 1])
emit('cancelled')
skipConfirmForLeavingEditor()
await tutorial.startCourse(nextCourse, currentSeries)
},
{
Expand Down Expand Up @@ -96,8 +112,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' }) }}
</UIButton>
<UIButton v-if="hasNextCourse" size="large" @click="handleStartNextCourse">
{{ $t({ zh: '学习下一个课程', en: 'Learn next course' }) }}
Expand Down