Skip to content

Commit 6919467

Browse files
authored
Merge pull request frappe#2426 from raizasafeel/feat/course-progress-settings
feat: progress control in settings
2 parents a3913f6 + a4ac02a commit 6919467

16 files changed

Lines changed: 1830 additions & 57 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Frontend Tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main, develop, main-hotfix]
7+
8+
jobs:
9+
vitest:
10+
name: Vitest
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-node@v6
16+
with:
17+
node-version: 24
18+
check-latest: true
19+
20+
- name: Cache yarn
21+
uses: actions/cache@v4
22+
with:
23+
path: ~/.cache/yarn
24+
key: ${{ runner.os }}-yarn-${{ hashFiles('frontend/yarn.lock') }}
25+
restore-keys: |
26+
${{ runner.os }}-yarn-
27+
28+
- name: Install dependencies
29+
working-directory: frontend
30+
run: yarn install --frozen-lockfile
31+
32+
- name: Run vitest
33+
working-directory: frontend
34+
run: yarn test --run

frontend/components.d.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/* eslint-disable */
22
// @ts-nocheck
3+
// biome-ignore lint: disable
4+
// oxlint-disable
5+
// ------
36
// Generated by unplugin-vue-components
47
// Read more: https://github.com/vuejs/core/pull/3399
5-
// biome-ignore lint: disable
8+
69
export {}
710

811
/* prettier-ignore */
@@ -39,9 +42,7 @@ declare module 'vue' {
3942
CouponItems: typeof import('./src/components/Settings/Coupons/CouponItems.vue')['default']
4043
CouponList: typeof import('./src/components/Settings/Coupons/CouponList.vue')['default']
4144
Coupons: typeof import('./src/components/Settings/Coupons/Coupons.vue')['default']
42-
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
4345
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
44-
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
4546
CourseOutline: typeof import('./src/components/CourseOutline.vue')['default']
4647
CourseReviews: typeof import('./src/components/CourseReviews.vue')['default']
4748
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
@@ -96,7 +97,6 @@ declare module 'vue' {
9697
QuizBlock: typeof import('./src/components/QuizBlock.vue')['default']
9798
QuizInVideo: typeof import('./src/components/Modals/QuizInVideo.vue')['default']
9899
Rating: typeof import('./src/components/Controls/Rating.vue')['default']
99-
RelatedCourses: typeof import('./src/components/RelatedCourses.vue')['default']
100100
ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default']
101101
RouterLink: typeof import('vue-router')['RouterLink']
102102
RouterView: typeof import('vue-router')['RouterView']
@@ -114,7 +114,6 @@ declare module 'vue' {
114114
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
115115
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']
116116
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
117-
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
118117
UserDropdown: typeof import('./src/components/Sidebar/UserDropdown.vue')['default']
119118
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
120119
VideoStatistics: typeof import('./src/components/Modals/VideoStatistics.vue')['default']

frontend/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"serve": "vite preview",
99
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry && yarn copy-colors-json",
1010
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/_lms.html",
11-
"copy-colors-json": "cp node_modules/frappe-ui/tailwind/colors.json src/utils/frappe-ui-colors.json"
11+
"copy-colors-json": "cp node_modules/frappe-ui/tailwind/colors.json src/utils/frappe-ui-colors.json",
12+
"test": "vitest run",
13+
"test:watch": "vitest"
1214
},
1315
"dependencies": {
1416
"@codemirror/lang-html": "6.4.9",
@@ -50,11 +52,14 @@
5052
},
5153
"devDependencies": {
5254
"@vitejs/plugin-vue": "5.0.3",
55+
"@vue/test-utils": "^2.4.10",
5356
"autoprefixer": "10.4.2",
57+
"jsdom": "^29.1.1",
5458
"postcss": "8.4.5",
5559
"tailwindcss": "^3.4.15",
5660
"unplugin-auto-import": "^20.3.0",
5761
"vite": "5.0.11",
58-
"vite-plugin-pwa": "^1.2.0"
62+
"vite-plugin-pwa": "^1.2.0",
63+
"vitest": "^4.1.7"
5964
}
6065
}

frontend/src/components/Settings/SettingDetails.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
<script setup>
3232
import { Button, Badge, toast } from 'frappe-ui'
3333
import SettingFields from '@/components/Settings/SettingFields.vue'
34+
import { useSettings } from '@/stores/settings'
35+
36+
const settingsStore = useSettings()
3437
3538
const props = defineProps({
3639
sections: {
@@ -54,6 +57,12 @@ const update = () => {
5457
props.data.save.submit(
5558
{},
5659
{
60+
onSuccess() {
61+
// Bust the cached get_lms_settings response so consumers
62+
// (Lesson.vue, sidebar, etc.) read fresh values without a
63+
// full page reload.
64+
settingsStore.settings.reload?.()
65+
},
5766
onError(err) {
5867
toast.error(err.messages?.[0] || err)
5968
},

frontend/src/components/Settings/SettingFields.vue

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@
55
{{ section.label }}
66
</div>
77
<div
8-
:class="{
9-
'flex justify-between gap-x-8 w-full': section.columns.length > 1,
10-
}"
8+
:class="
9+
section.columns.length > 1
10+
? 'grid grid-cols-2 gap-x-8 gap-y-5 w-full items-start'
11+
: 'w-full space-y-5'
12+
"
1113
>
12-
<div
13-
v-for="(column, index) in section.columns"
14-
class="w-full space-y-5"
14+
<template
15+
v-for="(column, columnIndex) in section.columns"
16+
:key="columnIndex"
1517
>
16-
<div v-for="field in column.fields">
18+
<div
19+
v-for="(field, fieldIndex) in column.fields"
20+
:key="`${columnIndex}-${fieldIndex}`"
21+
:style="
22+
section.columns.length > 1
23+
? { gridColumn: columnIndex + 1, gridRow: fieldIndex + 1 }
24+
: {}
25+
"
26+
>
1727
<Link
1828
v-if="field.type == 'Link'"
1929
v-model="data[field.name]"
@@ -111,7 +121,7 @@
111121
placeholder=""
112122
/>
113123
</div>
114-
</div>
124+
</template>
115125
</div>
116126
</div>
117127
</div>
@@ -135,15 +145,21 @@ const props = defineProps({
135145
},
136146
})
137147
148+
const resolveInitialValue = (field, dataValue) => {
149+
if (dataValue !== null && dataValue !== undefined && dataValue !== '') {
150+
return field.type === 'checkbox' ? !!dataValue : dataValue
151+
}
152+
if (field.default !== undefined) {
153+
return field.type === 'checkbox' ? !!field.default : field.default
154+
}
155+
return field.type === 'checkbox' ? false : ''
156+
}
157+
138158
onMounted(() => {
139159
props.sections.forEach((section) => {
140160
section.columns.forEach((column) => {
141161
column.fields.forEach((field) => {
142-
if (field.type == 'checkbox') {
143-
field.value = props.data[field.name] ? true : false
144-
} else {
145-
field.value = props.data[field.name]
146-
}
162+
field.value = resolveInitialValue(field, props.data[field.name])
147163
})
148164
})
149165
})
@@ -152,10 +168,14 @@ onMounted(() => {
152168
watch(
153169
props.sections,
154170
(newSections) => {
155-
// Makes the form dirty on change
171+
// Only checkboxes v-model on field.value; sync them to data so the
172+
// document resource sees the change. Non-checkbox fields v-model
173+
// directly against data and must NOT be touched here — otherwise the
174+
// stale field.value clobbers user input whenever any checkbox toggles.
156175
newSections.forEach((section) => {
157176
section.columns.forEach((column) => {
158177
column.fields.forEach((field) => {
178+
if (field.type !== 'checkbox') return
159179
if (props.data[field.name] != field.value) {
160180
props.data[field.name] = field.value
161181
}

frontend/src/components/Settings/Settings.vue

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,64 @@ const tabsStructure = computed(() => {
269269
},
270270
],
271271
},
272+
{
273+
label: 'Course Progress',
274+
icon: 'Activity',
275+
description:
276+
'Control how lessons are marked complete: dwell time and enforcement toggles for video, quiz, and assignment.',
277+
sections: [
278+
{
279+
label: 'Dwell Time',
280+
columns: [
281+
{
282+
fields: [
283+
{
284+
label: 'Lesson dwell time (seconds)',
285+
name: 'lesson_dwell_time',
286+
type: 'number',
287+
description:
288+
'Seconds a learner must stay on a lesson before it auto-marks complete.',
289+
},
290+
],
291+
},
292+
],
293+
},
294+
{
295+
label: 'Enforcement',
296+
columns: [
297+
{
298+
fields: [
299+
{
300+
label: 'Enforce video completion',
301+
name: 'enforce_video_completion',
302+
type: 'checkbox',
303+
description:
304+
'When enabled, lessons that contain a video can only be marked complete by playing the video to the end. If the video fails to load, the dwell timer is used as a fallback.',
305+
},
306+
{
307+
label: 'Enforce assignment completion',
308+
name: 'enforce_assignment_completion',
309+
type: 'checkbox',
310+
description:
311+
'When enabled, lessons with an assignment cannot be marked complete until the assignment is submitted.',
312+
},
313+
],
314+
},
315+
{
316+
fields: [
317+
{
318+
label: 'Enforce quiz completion',
319+
name: 'enforce_quiz_completion',
320+
type: 'checkbox',
321+
description:
322+
'When enabled, lessons with a quiz cannot be marked complete until the quiz is submitted.',
323+
},
324+
],
325+
},
326+
],
327+
},
328+
],
329+
},
272330
{
273331
label: 'Badges',
274332
description:

0 commit comments

Comments
 (0)