Merge pull request #2159 from pateljannat/demo-data

feat: demo data
This commit is contained in:
Jannat Patel
2026-03-09 16:47:08 +05:30
committed by GitHub
38 changed files with 781 additions and 254 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<FrappeUIProvider>
<Layout class="isolate text-base">
<Layout class="isolate text-p-base">
<router-view />
</Layout>
<InstallPrompt v-if="isMobile && !settings.data?.disable_pwa" />
@@ -90,21 +90,26 @@
{{ __('Get Certificate') }}
</Button>
</div>
<div class="space-y-4">
<div class="space-y-3">
<div class="font-medium text-ink-gray-9">
{{ __('This course has:') }}
</div>
<div class="flex items-center text-ink-gray-9">
<BookOpen class="h-4 w-4 stroke-1.5" />
<span class="ml-2">
{{ course.data.lessons }} {{ __('Lessons') }}
{{ course.data.lessons }}
{{ course.data.lessons > 1 ? __('lessons') : __('lesson') }}
</span>
</div>
<div class="flex items-center text-ink-gray-9">
<Users class="h-4 w-4 stroke-1.5" />
<span class="ml-2">
{{ formatAmount(course.data.enrollments) }}
{{ __('Enrolled Students') }}
{{
course.data.enrollments > 1
? __('enrolled students')
: __('enrolled student')
}}
</span>
</div>
<div
@@ -113,7 +118,7 @@
>
<Star class="size-4 stroke-1.5 fill-yellow-500 text-transparent" />
<span class="ml-2">
{{ course.data.rating }} {{ __('Rating') }}
{{ course.data.rating }} {{ __('average rating') }}
</span>
</div>
<div
+1 -3
View File
@@ -4,9 +4,7 @@
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
{{ __('No {0}').format(type?.toLowerCase()) }}
</div>
<div
class="leading-5 text-base w-full md:w-2/5 text-base text-center text-ink-gray-7"
>
<div class="text-p-base w-full md:w-2/5 text-center text-ink-gray-7">
{{
__(
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
@@ -14,7 +14,7 @@
: __('Edit Assignment')
}}
</div>
<div class="space-y-4 max-h-[75vh] overflow-y-auto">
<div class="space-y-4 max-h-[75vh] overflow-y-auto p-1">
<FormControl
v-model="assignment.title"
:label="__('Title')"
@@ -43,7 +43,7 @@
@change="(val) => (assignment.question = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[18rem] overflow-y-auto"
/>
</div>
</div>
@@ -73,7 +73,7 @@
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { computed, reactive, watch } from 'vue'
import { escapeHTML, sanitizeHTML } from '@/utils'
import { Link } from 'frappe-ui/frappe'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const assignments = defineModel<Assignments>('assignments')
+9 -12
View File
@@ -2,7 +2,7 @@
<Dialog
v-model="show"
:options="{
size: '5xl',
size: '3xl',
}"
>
<template #body>
@@ -10,17 +10,14 @@
<div class="text-lg font-semibold text-ink-gray-9 mb-5">
{{ __(props.title) }}
</div>
<div
<Switch
v-if="!editMode"
class="flex items-center text-xs text-ink-gray-7 space-x-5"
>
<Switch
size="sm"
:label="__('Choose an existing question')"
v-model="chooseFromExisting"
class="!p-0"
/>
</div>
size="sm"
:label="__('Choose an existing question')"
:description="__('Select from questions you have already created')"
v-model="chooseFromExisting"
class="!p-0"
/>
<div v-if="!chooseFromExisting || editMode">
<div>
<label class="block text-xs text-ink-gray-5 mb-1">
@@ -164,7 +161,7 @@ populateFields()
const props = defineProps({
title: {
type: String,
default: __('Add a new question'),
default: __('Add new question'),
},
questionDetail: {
type: [Object, null],
@@ -31,7 +31,7 @@
<Search class="size-4 stroke-1.5 text-ink-gray-5" />
</template>
</FormControl>
<div class="overflow-auto h-[60vh]">
<div class="overflow-auto max-h-[60vh]">
<div class="divide-y divide-outline-gray-modals">
<div
v-for="evaluator in evaluators.data"
+1 -1
View File
@@ -31,7 +31,7 @@
<Search class="size-4 stroke-1.5 text-ink-gray-5" />
</template>
</FormControl>
<div class="overflow-y-scroll h-[60vh]">
<div class="overflow-y-scroll max-h-[60vh]">
<ul class="divide-y divide-outline-gray-modals">
<li
v-for="member in memberList"
@@ -65,7 +65,7 @@
<script setup>
import { sessionStore } from '@/stores/session'
import { Dropdown } from 'frappe-ui'
import { call, Dropdown, toast } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { convertToTitleCase } from '@/utils'
import { usersStore } from '@/stores/user'
@@ -85,7 +85,7 @@ import {
User,
Settings,
Sun,
Zap,
Trash2,
} from 'lucide-vue-next'
const router = useRouter()
@@ -175,6 +175,19 @@ const userDropdownOptions = computed(() => {
return userResource.data?.is_moderator
},
},
{
label: 'Clear Demo Data',
icon: Trash2,
onClick: () => {
clearDemoDataConfirmation()
},
condition: () => {
return (
userResource.data?.is_moderator &&
settingsStore.settings.data?.demo_data_present
)
},
},
{
icon: FrappeCloudIcon,
label: 'Login to Frappe Cloud',
@@ -234,4 +247,36 @@ const loginToFrappeCloud = () => {
let redirect_to = '/dashboard/sites/' + userResource.data.sitename
window.open(`${frappeCloudBaseEndpoint}${redirect_to}`, '_blank')
}
const clearDemoDataConfirmation = () => {
$dialog({
title: __('Confirm clearing demo data?'),
message: __(
'Are you sure you want to clear the demo data? This would delete the course "A guide to Frappe Learning" along with all its associated data. This action cannot be undone.'
),
actions: [
{
label: __('Confirm'),
theme: 'red',
variant: 'solid',
onClick(close) {
clearDemoData()
close()
},
},
],
})
}
const clearDemoData = () => {
call('lms.lms.api.clear_demo_data')
.then(() => {
window.location.href = '/lms'
toast.success(__('Demo data cleared successfully'))
})
.catch((error) => {
toast.error(__(error.message || 'Error clearing demo data'))
console.error('Error clearing demo data:', error)
})
}
</script>
+4 -1
View File
@@ -18,7 +18,7 @@
</Button>
</div>
<Dropdown
v-else-if="isAdmin"
v-else-if="isAdmin && batchMenu.length"
:options="batchMenu"
placement="left"
side="left"
@@ -209,6 +209,9 @@ const canMakeAnnouncement = () => {
}
const batchMenu = computed(() => {
if (!batch.data?.certification && !canMakeAnnouncement()) {
return []
}
let options = [
{
label: __('Generate Certificates'),
@@ -56,7 +56,7 @@
</span>
</div>
<div v-if="!readOnlyMode">
<div v-if="!readOnlyMode && !canAccessBatch">
<router-link
:to="{
name: 'Billing',
@@ -71,7 +71,7 @@
batch.data.accept_enrollments
"
>
<Button v-if="!canAccessBatch" class="w-full mt-4" variant="solid">
<Button class="w-full mt-4" variant="solid">
<template #prefix>
<CreditCard class="size-4 stroke-1.5" />
</template>
@@ -173,14 +173,6 @@ const isEvaluator = computed(() => {
return user.data?.is_evaluator
})
const isInstructor = computed(() => {
return (
props.batch.data?.instructors?.filter(
(instructor) => instructor.name === user.data?.name
).length > 0
)
})
const canAccessBatch = computed(() => {
if (!user.data) {
return false
@@ -188,7 +180,7 @@ const canAccessBatch = computed(() => {
return isModerator.value || isStudent.value || isEvaluator.value
})
const canEditBatch = computed(() => {
return isModerator.value || isInstructor.value
const isAdmin = computed(() => {
return isModerator.value || isEvaluator.value
})
</script>
@@ -13,6 +13,7 @@
v-model="batch.title"
:label="__('Title')"
:required="true"
autocomplete="off"
/>
<FormControl
v-model="batch.start_date"
@@ -42,6 +43,7 @@
v-model="batch.timezone"
:label="__('Timezone')"
:required="true"
autocomplete="off"
/>
<Link
doctype="LMS Category"
@@ -72,6 +74,13 @@
<div class="space-y-5 border-t mt-5 pt-5">
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
:required="true"
:rows="4"
/>
<MultiSelect
v-model="batch.instructors"
doctype="Course Evaluator"
@@ -80,13 +89,6 @@
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
/>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
:required="true"
:rows="4"
/>
</div>
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
+9 -2
View File
@@ -1,7 +1,10 @@
<template>
<div class="pl-5">
<div class="grid grid-cols-1 md:grid-cols-[70%,30%]">
<div v-if="courseResource.doc" class="lg:max-h-[88vh] lg:overflow-y-auto">
<div
v-if="courseResource.doc"
class="lg:max-h-[88vh] lg:overflow-y-auto px-1"
>
<div class="my-5">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
@@ -71,7 +74,11 @@
<ColorSwatches
v-model="courseResource.doc.card_gradient"
:label="__('Color')"
:description="__('Choose a color for the course card')"
:description="
__(
'Select a fallback color for the course card when no image is set.'
)
"
class="w-full"
@update:modelValue="makeFormDirty()"
/>
+3
View File
@@ -87,6 +87,9 @@ const identifyUserPersona = async () => {
if (personaCaptured) return
let courseCount = await call('frappe.client.get_count', {
doctype: 'LMS Course',
filters: {
title: ['not like', '%A guide to Frappe Learning%'],
},
})
if (!courseCount) {
router.push({ name: 'PersonaForm' })
+18 -23
View File
@@ -65,7 +65,7 @@
</router-link>
</div>
</header>
<div class="grid md:grid-cols-[70%,30%] h-screen">
<div class="grid md:grid-cols-[70%,30%] h-[94vh]">
<div v-if="lesson.data.no_preview" class="border-r">
<div class="shadow rounded-md w-3/4 mt-10 mx-auto text-center p-4">
<div class="flex items-center justify-center mt-4 space-x-2">
@@ -263,7 +263,7 @@
</div>
</div>
<div
v-if="lesson.data"
v-if="lesson.data && (allowDiscussions || tabs.length > 1)"
class="mt-10 pb-20 pt-5 border-t px-5"
ref="discussionsContainer"
>
@@ -399,15 +399,10 @@ const { brand } = sessionStore()
const sidebarStore = useSidebar()
const plyrSources = ref([])
const showInlineMenu = ref(false)
const currentTab = ref('Notes')
const currentTab = ref(null)
let timerInterval = null
const tabs = ref([
{
label: __('Notes'),
value: 'Notes',
},
])
const tabs = ref([])
const props = defineProps({
courseName: {
@@ -887,24 +882,24 @@ const updateNotes = () => {
}
watch(allowDiscussions, () => {
if (allowDiscussions.value) {
tabs.value = [
{
if (!isAdmin.value) {
if (!tabs.value.find((tab) => tab.value === 'Notes')) {
tabs.value.push({
label: __('Notes'),
value: 'Notes',
},
{
})
}
currentTab.value = 'Notes'
} else {
currentTab.value = allowDiscussions.value ? 'Community' : null
}
if (allowDiscussions.value) {
if (!tabs.value.find((tab) => tab.value === 'Community')) {
tabs.value.push({
label: __('Community'),
value: 'Community',
},
]
} else {
tabs.value = [
{
label: __('Notes'),
value: 'Notes',
},
]
})
}
}
})
+5
View File
@@ -199,11 +199,16 @@ const evaluator = createResource({
if (data.slots.unavailable_from) from.value = data.slots.unavailable_from
if (data.slots.unavailable_to) to.value = data.slots.unavailable_to
},
onError(err) {
toast.error(err.messages?.[0] || err)
console.error(err)
},
})
const createSlot = createResource({
url: 'frappe.client.insert',
makeParams(values) {
console.log(evaluator.data)
return {
doc: {
doctype: 'Evaluator Schedule',
@@ -51,7 +51,7 @@
@change="(val: string) => (exercise.problem_statement = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[21rem] overflow-y-auto"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[21rem] overflow-y-auto"
/>
</div>
</div>
@@ -71,6 +71,7 @@
{{ __('Delete') }}
</Button>
<router-link
v-if="exerciseID != 'new'"
:to="{
name: 'ProgrammingExerciseSubmission',
params: {
@@ -87,6 +88,7 @@
</Button>
</router-link>
<router-link
v-if="exerciseID != 'new'"
:to="{
name: 'ProgrammingExerciseSubmissions',
query: {
@@ -148,6 +150,7 @@ const languageOptions = [
const props = withDefaults(
defineProps<{
exerciseID: string
getExerciseCount: () => Promise<number>
}>(),
{
exerciseID: 'new',
@@ -185,7 +188,6 @@ const setExerciseData = () => {
const testCases = createListResource({
doctype: 'LMS Test Case',
fields: ['input', 'expected_output', 'name'],
cache: ['testCases', props.exerciseID],
parent: 'LMS Programming Exercise',
orderBy: 'idx',
onSuccess(data: TestCase[]) {
@@ -207,7 +209,7 @@ const fetchTestCases = () => {
},
})
testCases.reload()
originalTestCaseCount.value = testCases.data.length
originalTestCaseCount.value = testCases.data?.length
}
const validateTitle = () => {
@@ -223,7 +225,7 @@ watch(
)
watch(testCases, () => {
if (testCases.data.length !== originalTestCaseCount.value) {
if (testCases.data?.length !== originalTestCaseCount.value) {
isDirty.value = true
}
})
@@ -255,6 +257,7 @@ const createNewExercise = (close: () => void) => {
close()
isDirty.value = false
exercises.value?.reload()
props.getExerciseCount()
toast.success(__('Programming Exercise created successfully'))
},
onError(err: any) {
@@ -300,7 +300,7 @@ const loadFalcon = () => {
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = `${falconURL.value}static/livecode.js`
script.src = `${falconURL.value}/static/livecode.js`
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
@@ -5,6 +5,7 @@
<Breadcrumbs :items="breadcrumbs" />
<div class="space-x-2">
<router-link
v-if="exercises.data?.length"
:to="{
name: 'ProgrammingExerciseSubmissions',
}"
@@ -120,8 +121,9 @@
</div>
<ProgrammingExerciseForm
v-model="showForm"
:exerciseID="exerciseID"
v-model:exercises="exercises"
:exerciseID="exerciseID"
:getExerciseCount="getExerciseCount"
/>
</template>
<script setup lang="ts">
@@ -152,7 +154,7 @@ const exerciseCount = ref<number>(0)
const readOnlyMode = window.read_only_mode
const { brand } = sessionStore()
const showForm = ref<boolean>(false)
const exerciseID = ref<string | null>('new')
const exerciseID = ref<string>('new')
const user = inject<any>('$user')
const titleFilter = ref<string>('')
const languageFilter = ref<string>('')
+1 -5
View File
@@ -194,11 +194,7 @@
v-model="showQuestionModal"
:questionDetail="currentQuestion"
v-model:quiz="quizDetails"
:title="
currentQuestion.question
? __('Edit the question')
: __('Add a new question')
"
:title="currentQuestion.question ? __('Edit Question') : __('Add Question')"
/>
</template>
<script setup>
+3 -6
View File
@@ -12,12 +12,8 @@
</header>
<div class="py-5 mx-5">
<div class="flex items-center justify-between mb-4">
<div class="text-lg font-semibold text-ink-gray-7">
{{
quizzes.data?.length
? __('{0} Quizzes').format(quizzes.data.length)
: __('No Quizzes')
}}
<div class="text-lg font-semibold">
{{ __('{0} Quizzes').format(quizzes.data.length) }}
</div>
<FormControl v-model="search" type="text" placeholder="Search">
<template #prefix>
@@ -116,6 +112,7 @@
v-model="title"
:label="__('Title')"
type="text"
autocomplete="off"
@keydown.enter="insertQuiz(() => (showForm = false))"
/>
</template>