Skip to content

Commit 45e5c3b

Browse files
committed
Merge branch 'main-hotfix' of https://github.com/frappe/lms into sync/main-hotfix-to-main-v2
# Conflicts: # pyproject.toml
2 parents 702ca04 + f3fd3b5 commit 45e5c3b

160 files changed

Lines changed: 11517 additions & 1705 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.

.github/semgrep/tailwind-rtl.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Local run: semgrep scan --config .github/semgrep/tailwind-rtl.yml
2+
rules:
3+
- id: tailwind-physical-class
4+
message: |
5+
RTL: replace physical Tailwind class with logical equivalent.
6+
languages: [generic]
7+
severity: WARNING
8+
paths: { include: ["*.vue"] }
9+
patterns:
10+
- pattern-either:
11+
- pattern-regex: |
12+
(?x)
13+
(?:^|[\s"'`(\[{,])
14+
!?
15+
(?:[^\s"':]+:)*
16+
-?
17+
(?:
18+
(?:scroll-)?[mp][lr]-(?:auto|px|full|\d+(?:[./]\d+)?|\[[^\]]+\])
19+
| (?:left|right)-(?:auto|px|full|\d+(?:[./]\d+)?|\[[^\]]+\])
20+
| (?:text|float|clear)-(?:left|right)
21+
| (?:border|rounded)-[lr](?:-[A-Za-z0-9\[\]./#%_-]+)?
22+
| rounded-(?:tl|tr|bl|br)(?:-[A-Za-z0-9\[\]./#%_-]+)?
23+
)
24+
!?
25+
(?=$|[\s'"`})\],>;])
26+
- pattern-not-regex: '\b(?:rtl|ltr):'
27+
28+
- id: tailwind-space-x-needs-reverse-or-gap
29+
message: |
30+
RTL: `space-x-*` does not flip in RTL. add `rtl:space-x-reverse` or `gap-x-* ms-*`
31+
languages: [generic]
32+
severity: WARNING
33+
paths: { include: ["*.vue"] }
34+
patterns:
35+
- pattern-regex: 'class\s*[:=]\s*"[^"]*\bspace-x-\S+[^"]*"'
36+
- pattern-not-regex: '\brtl:space-x-reverse\b'
37+
38+
- id: tailwind-divide-x-needs-reverse
39+
message: |
40+
RTL: `divide-x-*` does not flip in RTL, sdd `rtl:divide-x-reverse`.
41+
languages: [generic]
42+
severity: WARNING
43+
paths: { include: ["*.vue"] }
44+
patterns:
45+
- pattern-regex: 'class\s*[:=]\s*"[^"]*\bdivide-x(?:-\S+)?[^"]*"'
46+
- pattern-not-regex: '\brtl:divide-x-reverse\b'

.github/workflows/linters.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,7 @@ jobs:
5858
run: pip install semgrep
5959

6060
- name: Run Semgrep rules
61-
run: semgrep ci --config ./frappe-semgrep-rules/rules
61+
run: semgrep ci --config ./frappe-semgrep-rules/rules
62+
63+
- name: Run RTL Semgrep rules
64+
run: semgrep scan --config ./.github/semgrep/tailwind-rtl.yml

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,19 @@ You need Docker, docker-compose and git setup on your machine. Refer [Docker doc
152152
To setup the repository locally follow the steps mentioned below:
153153

154154
1. Install bench and setup a `frappe-bench` directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation)
155-
1. Start the server by running `bench start`
156-
1. In a separate terminal window, create a new site by running `bench new-site learning.test`
157-
1. Map your site to localhost with the command `bench --site learning.test add-to-hosts`
158-
1. Get the Learning app. Run `bench get-app https://github.com/frappe/lms`
159-
1. Run `bench --site learning.test install-app lms`.
155+
1. Start the server by running
156+
```sh
157+
$ bench start
158+
```
159+
1. In a separate terminal window, run the following commands.
160+
```sh
161+
$ bench new-site learning.test
162+
$ bench --site learning.test add-to-hosts
163+
$ bench get-app https://github.com/frappe/payments
164+
$ bench get-app https://github.com/frappe/lms
165+
$ bench --site learning.test install-app lms
166+
167+
```
160168
1. Now open the URL `http://learning.test:8000/lms` in your browser, you should see the app running
161169

162170
## Learn and connect

cypress/e2e/batch_creation.cy.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe("Batch Creation", () => {
77

88
// Open Settings
99
cy.get("span").contains("Learning").click();
10-
cy.get("span").contains("Settings").click();
10+
cy.contains('[role="menuitem"]', "Settings").click();
1111

1212
// Add a new member
1313
cy.get("[data-dismissable-layer]")
@@ -38,7 +38,7 @@ describe("Batch Creation", () => {
3838
.find("button")
3939
.contains("New")
4040
.click();
41-
cy.get("span").contains("New Evaluator").click();
41+
cy.contains('[role="menuitem"]', "New Evaluator").click();
4242

4343
const randomEvaluator = `evaluator${dateNow}@example.com`;
4444
cy.get("input[placeholder='jane@doe.com']").type(randomEvaluator);
@@ -52,7 +52,7 @@ describe("Batch Creation", () => {
5252

5353
// Create a batch
5454
cy.get("button").contains("Create").click();
55-
cy.get("span").contains("New Batch").click();
55+
cy.contains('[role="menuitem"]', "New Batch").click();
5656
cy.wait(500);
5757
cy.get("label").contains("Title").type("Test Batch");
5858
cy.get("label").contains("Start Date").type("2030-10-01");
@@ -65,7 +65,7 @@ describe("Batch Creation", () => {
6565
cy.get("label")
6666
.contains("Description")
6767
.type("Test Batch Short Description to test the UI");
68-
cy.get("div[contenteditable=true").invoke(
68+
cy.get("div.ProseMirror").invoke(
6969
"text",
7070
"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."
7171
);
@@ -74,7 +74,7 @@ describe("Batch Creation", () => {
7474
.contains("Instructors")
7575
.parent()
7676
.within(() => {
77-
cy.get("input").click().type("evaluator");
77+
cy.get("input").click().clear().type(randomEvaluator);
7878
cy.get("input")
7979
.invoke("attr", "aria-controls")
8080
.as("instructor_list_id");
@@ -87,7 +87,20 @@ describe("Batch Creation", () => {
8787
});
8888
});
8989
cy.button("Save").click();
90-
cy.get("label").contains("Published").click();
90+
cy.wait(1000);
91+
92+
// going to batch settings and publishing the batch
93+
cy.url().should("include", "#settings");
94+
cy.closeOnboardingModal();
95+
cy.contains("label", "Published")
96+
.invoke("attr", "for")
97+
.then((id) => {
98+
cy.get(`#${id}`)
99+
.scrollIntoView()
100+
.should("be.visible")
101+
.click({ force: true });
102+
cy.get(`#${id}`).should("have.attr", "aria-checked", "true");
103+
});
91104
cy.button("Save").click();
92105
cy.wait(1000);
93106
let batchName;
@@ -105,11 +118,7 @@ describe("Batch Creation", () => {
105118

106119
cy.url().should("include", "/lms/batches");
107120

108-
cy.get('[id^="headlessui-radiogroup-v-"]')
109-
.find("span")
110-
.contains("Upcoming")
111-
.should("be.visible")
112-
.click();
121+
cy.contains('[role="radio"]', "Upcoming").should("be.visible").click();
113122

114123
cy.get("@batchName").then((batchName) => {
115124
cy.get(`a[href='/lms/batches/${batchName}'`).within(() => {
@@ -154,6 +163,7 @@ describe("Batch Creation", () => {
154163
cy.get("button:visible").contains("Dashboard").click();
155164

156165
/* Add student to batch */
166+
cy.closeOnboardingModal();
157167
cy.get("button").contains("Enroll").click();
158168
cy.get('div[role="dialog"]')
159169
.first()

cypress/e2e/course_creation.cy.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ describe("Course Creation", () => {
99

1010
// Create a course
1111
cy.get("button").contains("Create").click();
12-
cy.get("span").contains("New Course").click();
12+
cy.contains('[role="menuitem"]', "New Course").click();
1313
cy.wait(500);
1414

1515
cy.get("label").contains("Title").type("Test Course");
1616
cy.get("label")
1717
.contains("Short Introduction")
1818
.type("Test Course Short Introduction to test the UI");
19-
cy.get("div[contenteditable=true").invoke(
19+
cy.get("div.ProseMirror").invoke(
2020
"text",
2121
"Test Course 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."
2222
);
@@ -61,7 +61,7 @@ describe("Course Creation", () => {
6161
});
6262

6363
cy.button("Save").last().click();
64-
64+
cy.closeOnboardingModal();
6565
// Edit Course Details
6666
cy.wait(500);
6767
cy.get("label")
@@ -153,7 +153,7 @@ describe("Course Creation", () => {
153153
cy.wait(500);
154154
cy.get("[data-dismissable-layer]").within(() => {
155155
cy.get("label").contains("Title").type("Test Discussion");
156-
cy.get("div[contenteditable=true]").invoke(
156+
cy.get("div.ProseMirror").invoke(
157157
"text",
158158
"This is a test discussion. This will check if the UI is working properly."
159159
);
@@ -163,7 +163,7 @@ describe("Course Creation", () => {
163163
// View Discussion
164164
cy.wait(500);
165165
cy.get("div").contains("Test Discussion").click();
166-
cy.get("div[contenteditable=true").invoke(
166+
cy.get("div.ProseMirror").invoke(
167167
"text",
168168
"This is a test comment. This will check if the UI is working properly."
169169
);
@@ -179,7 +179,7 @@ describe("Course Creation", () => {
179179
cy.get("svg.lucide.lucide-ellipsis-icon").click();
180180
});
181181
cy.get("div[role=menu]").within(() => {
182-
cy.get("span").contains("Delete").click();
182+
cy.contains('[role="menuitem"]', "Delete").click();
183183
});
184184
cy.get("span").contains("Delete").click();
185185
cy.wait(500);

cypress/support/commands.js

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

7373
Cypress.Commands.add("closeOnboardingModal", () => {
7474
cy.wait(500);
75+
const modalSelector = '[data-testid="onboarding-help-modal"]';
7576
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.");
77+
if (!$body.find(modalSelector).length) {
78+
cy.log("Onboarding modal not present, skipping close.");
79+
return;
8480
}
81+
82+
// Skip onboarding steps if the button exists, otherwise just close the modal.
83+
if ($body.find(`${modalSelector} button:contains("Skip all")`).length) {
84+
cy.get(modalSelector).contains("button", "Skip all").click();
85+
}
86+
87+
cy.get(modalSelector).find("button:has(svg.feather-x)").click();
88+
cy.get(modalSelector).should("not.exist");
8589
});
8690
});

frontend/components.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ declare module 'vue' {
4646
CourseReviews: typeof import('./src/components/CourseReviews.vue')['default']
4747
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
4848
DateRange: typeof import('./src/components/Common/DateRange.vue')['default']
49-
DesktopLayout: typeof import('./src/components/DesktopLayout.vue')['default']
49+
DesktopLayout: typeof import('./src/components/Layouts/DesktopLayout.vue')['default']
5050
DiscussionModal: typeof import('./src/components/Modals/DiscussionModal.vue')['default']
5151
DiscussionReplies: typeof import('./src/components/DiscussionReplies.vue')['default']
5252
Discussions: typeof import('./src/components/Discussions.vue')['default']
5353
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
5454
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
5555
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
5656
EmailTemplates: typeof import('./src/components/Settings/EmailTemplates.vue')['default']
57-
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
57+
EmptyStateLayout: typeof import('./src/components/Layouts/EmptyStateLayout.vue')['default']
5858
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
5959
Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default']
6060
Event: typeof import('./src/components/Modals/Event.vue')['default']
@@ -70,18 +70,19 @@ declare module 'vue' {
7070
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
7171
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
7272
JobCard: typeof import('./src/components/JobCard.vue')['default']
73+
LayoutHeader: typeof import('./src/components/Layouts/LayoutHeader.vue')['default']
7374
LessonContent: typeof import('./src/components/LessonContent.vue')['default']
7475
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
7576
Link: typeof import('./src/components/Controls/Link.vue')['default']
7677
LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default']
7778
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
7879
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
7980
Members: typeof import('./src/components/Settings/Members.vue')['default']
80-
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
81+
MobileLayout: typeof import('./src/components/Layouts/MobileLayout.vue')['default']
8182
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
8283
NewMemberModal: typeof import('./src/components/Modals/NewMemberModal.vue')['default']
8384
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
84-
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
85+
NoSidebarLayout: typeof import('./src/components/Layouts/NoSidebarLayout.vue')['default']
8586
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
8687
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
8788
NumberChartGraph: typeof import('./src/components/NumberChartGraph.vue')['default']

frontend/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="{{ boot.lang }}" dir="{{ boot.text_direction }}">
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" href="{{ favicon }}" />

frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"dayjs": "1.11.10",
3232
"dompurify": "3.2.6",
3333
"feather-icons": "4.28.0",
34-
"frappe-ui": "^0.1.264",
34+
"frappe-ui": "^0.1.276",
3535
"highlight.js": "11.11.1",
3636
"lucide-vue-next": "0.383.0",
3737
"markdown-it": "14.0.0",
@@ -49,7 +49,7 @@
4949
"vuedraggable": "4.1.0"
5050
},
5151
"devDependencies": {
52-
"@vitejs/plugin-vue": "5.0.3",
52+
"@vitejs/plugin-vue": "5.0.3",
5353
"autoprefixer": "10.4.2",
5454
"postcss": "8.4.5",
5555
"tailwindcss": "^3.4.15",

frontend/src/App.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import { computed, onUnmounted, ref } from 'vue'
1414
import { useScreenSize } from './utils/composables'
1515
import { useSettings } from '@/stores/settings'
1616
import { useRouter } from 'vue-router'
17-
import DesktopLayout from './components/DesktopLayout.vue'
18-
import MobileLayout from './components/MobileLayout.vue'
19-
import NoSidebarLayout from './components/NoSidebarLayout.vue'
17+
import DesktopLayout from './components/Layouts/DesktopLayout.vue'
18+
import MobileLayout from './components/Layouts/MobileLayout.vue'
19+
import NoSidebarLayout from './components/Layouts/NoSidebarLayout.vue'
2020
import InstallPrompt from './components/InstallPrompt.vue'
2121
2222
const { isMobile } = useScreenSize()

0 commit comments

Comments
 (0)