Skip to content

Commit 5212122

Browse files
authored
Merge pull request #1570 from frappe/develop
chore: merge 'develop' into 'main'
2 parents 2170819 + be8d985 commit 5212122

101 files changed

Lines changed: 13757 additions & 5719 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cypress/e2e/batch_creation.cy.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
describe("Batch Creation", () => {
2+
it("creates a new batch", () => {
3+
cy.login();
4+
cy.wait(500);
5+
cy.visit("/lms/batches");
6+
cy.closeOnboardingModal();
7+
8+
// Open Settings
9+
cy.get("span").contains("Learning").click();
10+
cy.get("span").contains("Settings").click();
11+
12+
// Add a new member
13+
cy.get('[id^="headlessui-dialog-panel-v-"]')
14+
.find("span")
15+
.contains(/^Members$/)
16+
.click();
17+
cy.get('[id^="headlessui-dialog-panel-v-"]')
18+
.find("button")
19+
.contains("New")
20+
.click();
21+
22+
const dateNow = Date.now();
23+
const randomEmail = `testuser_${dateNow}@example.com`;
24+
const randomName = `Test User ${dateNow}`;
25+
26+
cy.get("input[placeholder='Email']").type(randomEmail);
27+
cy.get("input[placeholder='First Name']").type(randomName);
28+
cy.get("button").contains("Add").click();
29+
30+
// Add evaluator
31+
cy.get('[id^="headlessui-dialog-panel-v-"]')
32+
.find("span")
33+
.contains(/^Evaluators$/)
34+
.click();
35+
36+
cy.get('[id^="headlessui-dialog-panel-v-"]')
37+
.find("button")
38+
.contains("New")
39+
.click();
40+
const randomEvaluator = `evaluator${dateNow}@example.com`;
41+
42+
cy.get("input[placeholder='Email']").type(randomEvaluator);
43+
cy.get("button").contains("Add").click();
44+
cy.get("div").contains(randomEvaluator).should("be.visible").click();
45+
46+
cy.visit("/lms/batches");
47+
cy.closeOnboardingModal();
48+
49+
// Create a batch
50+
cy.get("button").contains("New").click();
51+
cy.wait(500);
52+
cy.url().should("include", "/batches/new/edit");
53+
cy.get("label").contains("Title").type("Test Batch");
54+
55+
cy.get("label").contains("Start Date").type("2030-10-01");
56+
cy.get("label").contains("End Date").type("2030-10-31");
57+
cy.get("label").contains("Start Time").type("10:00");
58+
cy.get("label").contains("End Time").type("11:00");
59+
cy.get("label").contains("Timezone").type("IST");
60+
cy.get("label").contains("Seat Count").type("10");
61+
cy.get("label").contains("Published").click();
62+
63+
cy.get("label")
64+
.contains("Short Description")
65+
.type("Test Batch Short Description to test the UI");
66+
cy.get("div[contenteditable=true").invoke(
67+
"text",
68+
"Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
69+
);
70+
71+
/* Instructor */
72+
cy.get("label")
73+
.contains("Instructors")
74+
.parent()
75+
.within(() => {
76+
cy.get("input").click().type("evaluator");
77+
cy.get("input")
78+
.invoke("attr", "aria-controls")
79+
.as("instructor_list_id");
80+
});
81+
cy.get("@instructor_list_id").then((instructor_list_id) => {
82+
cy.get(`[id^=${instructor_list_id}`)
83+
.should("be.visible")
84+
.within(() => {
85+
cy.get("[id^=headlessui-combobox-option-").first().click();
86+
});
87+
});
88+
89+
cy.button("Save").click();
90+
cy.wait(1000);
91+
let batchName;
92+
cy.url().then((url) => {
93+
console.log(url);
94+
batchName = url.split("/").pop();
95+
cy.wrap(batchName).as("batchName");
96+
});
97+
cy.wait(500);
98+
99+
// View Batch
100+
cy.wait(1000);
101+
cy.visit("/lms/batches");
102+
cy.closeOnboardingModal();
103+
104+
cy.url().should("include", "/lms/batches");
105+
106+
cy.get('[id^="headlessui-radiogroup-v-"]')
107+
.find("span")
108+
.contains("Upcoming")
109+
.should("be.visible")
110+
.click();
111+
112+
cy.get("@batchName").then((batchName) => {
113+
cy.get(`a[href='/lms/batches/details/${batchName}'`).within(() => {
114+
cy.get("div").contains("Test Batch").should("be.visible");
115+
cy.get("div")
116+
.contains("Test Batch Short Description to test the UI")
117+
.should("be.visible");
118+
cy.get("span")
119+
.contains("01 Oct 2030 - 31 Oct 2030")
120+
.should("be.visible");
121+
cy.get("span")
122+
.contains("10:00 AM - 11:00 AM")
123+
.should("be.visible");
124+
cy.get("span").contains("IST").should("be.visible");
125+
cy.get("a").contains("Evaluator").should("be.visible");
126+
cy.get("div")
127+
.contains("10")
128+
.should("be.visible")
129+
.get("span")
130+
.contains("Seats Left")
131+
.should("be.visible");
132+
});
133+
cy.get(`a[href='/lms/batches/details/${batchName}'`).click();
134+
});
135+
136+
cy.get("div").contains("Test Batch").should("be.visible");
137+
cy.get("div")
138+
.contains("Test Batch Short Description to test the UI")
139+
.should("be.visible");
140+
cy.get("a").contains("Evaluator").should("be.visible");
141+
cy.get("span")
142+
.contains("01 Oct 2030 - 31 Oct 2030")
143+
.should("be.visible");
144+
cy.get("span").contains("10:00 AM - 11:00 AM").should("be.visible");
145+
cy.get("span").contains("IST").should("be.visible");
146+
cy.get("div")
147+
.contains("10")
148+
.should("be.visible")
149+
.get("span")
150+
.contains("Seats Left")
151+
.should("be.visible");
152+
153+
cy.get("p")
154+
.contains(
155+
"Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
156+
)
157+
.should("be.visible");
158+
cy.get("button").contains("Manage Batch").click();
159+
160+
/* Add student to batch */
161+
cy.get("button").contains("Add").click();
162+
cy.get('div[id^="headlessui-dialog-panel-v-"]')
163+
.first()
164+
.find("button")
165+
.eq(1)
166+
.click();
167+
cy.get("input[id^='headlessui-combobox-input-v-']").type(randomEmail);
168+
cy.get("div").contains(randomEmail).click();
169+
cy.get("button").contains("Submit").click();
170+
171+
// Verify Seat Count
172+
cy.get("span").contains("Details").click();
173+
cy.get("div")
174+
.contains("9")
175+
.should("be.visible")
176+
.get("span")
177+
.contains("Seats Left")
178+
.should("be.visible");
179+
});
180+
});

cypress/support/commands.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,15 @@ Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => {
7272

7373
Cypress.Commands.add("closeOnboardingModal", () => {
7474
cy.wait(500);
75-
cy.get('[class*="z-50"]')
76-
.find('button:has(svg[class*="feather-x"])')
77-
.realClick();
78-
cy.wait(1000);
75+
cy.get("body").then(($body) => {
76+
// Check if any element with class including 'z-50' exists
77+
if ($body.find('[class*="z-50"]').length > 0) {
78+
cy.get('[class*="z-50"]')
79+
.find('button:has(svg[class*="feather-x"])')
80+
.realClick();
81+
cy.wait(1000);
82+
} else {
83+
cy.log("Onboarding modal not found, skipping close.");
84+
}
85+
});
7986
});

frontend/components.d.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ declare module 'vue' {
2727
BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default']
2828
BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default']
2929
BatchStudents: typeof import('./src/components/BatchStudents.vue')['default']
30-
BrandSettings: typeof import('./src/components/BrandSettings.vue')['default']
30+
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
3131
BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default']
32-
Categories: typeof import('./src/components/Categories.vue')['default']
32+
Categories: typeof import('./src/components/Settings/Categories.vue')['default']
3333
CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default']
3434
ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default']
3535
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
@@ -48,10 +48,10 @@ declare module 'vue' {
4848
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
4949
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
5050
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
51-
EmailTemplates: typeof import('./src/components/EmailTemplates.vue')['default']
51+
EmailTemplates: typeof import('./src/components/Settings/EmailTemplates.vue')['default']
5252
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
5353
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
54-
Evaluators: typeof import('./src/components/Evaluators.vue')['default']
54+
Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default']
5555
Event: typeof import('./src/components/Modals/Event.vue')['default']
5656
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
5757
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
@@ -65,28 +65,30 @@ declare module 'vue' {
6565
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
6666
Link: typeof import('./src/components/Controls/Link.vue')['default']
6767
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
68+
LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default']
6869
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
6970
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
70-
Members: typeof import('./src/components/Members.vue')['default']
71+
Members: typeof import('./src/components/Settings/Members.vue')['default']
7172
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
7273
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
7374
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
7475
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
7576
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
7677
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
77-
PaymentSettings: typeof import('./src/components/PaymentSettings.vue')['default']
78+
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
7879
Play: typeof import('./src/components/Icons/Play.vue')['default']
7980
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
8081
Question: typeof import('./src/components/Modals/Question.vue')['default']
8182
Quiz: typeof import('./src/components/Quiz.vue')['default']
8283
QuizBlock: typeof import('./src/components/QuizBlock.vue')['default']
84+
QuizInVideo: typeof import('./src/components/Modals/QuizInVideo.vue')['default']
8385
Rating: typeof import('./src/components/Controls/Rating.vue')['default']
8486
ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default']
8587
RouterLink: typeof import('vue-router')['RouterLink']
8688
RouterView: typeof import('vue-router')['RouterView']
87-
SettingDetails: typeof import('./src/components/SettingDetails.vue')['default']
88-
SettingFields: typeof import('./src/components/SettingFields.vue')['default']
89-
Settings: typeof import('./src/components/Modals/Settings.vue')['default']
89+
SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default']
90+
SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default']
91+
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
9092
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
9193
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
9294
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
@@ -97,5 +99,7 @@ declare module 'vue' {
9799
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
98100
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
99101
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
102+
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
103+
ZoomSettings: typeof import('./src/components/Settings/ZoomSettings.vue')['default']
100104
}
101105
}

frontend/src/App.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ const Layout = computed(() => {
4545
4646
onUnmounted(() => {
4747
noSidebar.value = false
48-
stopSession()
4948
})
5049
5150
watch(userResource, () => {

frontend/src/components/AppSidebar.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,16 @@
181181
import UserDropdown from '@/components/UserDropdown.vue'
182182
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
183183
import SidebarLink from '@/components/SidebarLink.vue'
184-
import { ref, onMounted, inject, watch, reactive, markRaw, h } from 'vue'
184+
import {
185+
ref,
186+
onMounted,
187+
inject,
188+
watch,
189+
reactive,
190+
markRaw,
191+
h,
192+
onUnmounted,
193+
} from 'vue'
185194
import { getSidebarLinks } from '../utils'
186195
import { usersStore } from '@/stores/user'
187196
import { sessionStore } from '@/stores/session'
@@ -626,4 +635,8 @@ watch(userResource, () => {
626635
const redirectToWebsite = () => {
627636
window.open('https://frappe.io/learning', '_blank')
628637
}
638+
639+
onUnmounted(() => {
640+
socket.off('publish_lms_notifications')
641+
})
629642
</script>

frontend/src/components/BatchCard.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div
3-
class="flex flex-col border hover:border-outline-gray-4 rounded-md p-4 h-full"
3+
class="flex flex-col border hover:border-outline-gray-3 rounded-md p-4 h-full"
44
style="min-height: 150px"
55
>
66
<div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9">

frontend/src/components/BatchCourses.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ const courses = createResource({
106106
params: {
107107
batch: props.batch,
108108
},
109-
cache: ['batchCourses', props.batchName],
110109
auto: true,
111110
})
112111

frontend/src/components/Controls/MultiSelect.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@
5555
</div>
5656
</li>
5757
</ComboboxOption>
58+
<div class="h-10"></div>
5859
<div
5960
v-if="attrs.onCreate"
60-
class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t"
61+
class="absolute bottom-2 left-1 w-[99%] pt-2 bg-white border-t"
6162
>
6263
<Button
6364
variant="ghost"

frontend/src/components/CourseCardOverlay.vue

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
<script setup>
147147
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
148148
import { computed, inject } from 'vue'
149-
import { Badge, Button, createResource, toast } from 'frappe-ui'
149+
import { Badge, Button, call, createResource, toast } from 'frappe-ui'
150150
import { formatAmount } from '@/utils/'
151151
import { capture } from '@/telemetry'
152152
import { useRouter } from 'vue-router'
@@ -175,15 +175,11 @@ function enrollStudent() {
175175
toast.success(__('You need to login first to enroll for this course'))
176176
setTimeout(() => {
177177
window.location.href = `/login?redirect-to=${window.location.pathname}`
178-
}, 1000)
178+
}, 500)
179179
} else {
180-
const enrollStudentResource = createResource({
181-
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
180+
call('lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership', {
181+
course: props.course.data.name,
182182
})
183-
enrollStudentResource
184-
.submit({
185-
course: props.course.data.name,
186-
})
187183
.then(() => {
188184
capture('enrolled_in_course', {
189185
course: props.course.data.name,
@@ -198,7 +194,11 @@ function enrollStudent() {
198194
lessonNumber: 1,
199195
},
200196
})
201-
}, 2000)
197+
}, 1000)
198+
})
199+
.catch((err) => {
200+
toast.warning(__(err.messages?.[0] || err))
201+
console.error(err)
202202
})
203203
}
204204
}

frontend/src/components/DiscussionReplies.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
9797
import { timeAgo } from '../utils'
9898
import UserAvatar from '@/components/UserAvatar.vue'
9999
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
100-
import { ref, inject, onMounted } from 'vue'
100+
import { ref, inject, onMounted, onUnmounted } from 'vue'
101101
102102
const showTopics = defineModel('showTopics')
103103
const newReply = ref('')
@@ -251,4 +251,10 @@ const deleteReply = (reply) => {
251251
}
252252
)
253253
}
254+
255+
onUnmounted(() => {
256+
socket.off('publish_message')
257+
socket.off('update_message')
258+
socket.off('delete_message')
259+
})
254260
</script>

0 commit comments

Comments
 (0)