Compare commits

..

697 Commits

Author SHA1 Message Date
Jannat Patel 224bb18d3e Merge pull request #1077 from pateljannat/issues-45
fix: show live class start button only to moderators and evaluators
2024-10-23 12:53:42 +05:30
Jannat Patel aab7bdcc20 fix: show live class start button only to moderators and evaluators 2024-10-23 11:02:16 +05:30
Jannat Patel c5ca428d98 Merge pull request #1076 from pateljannat/issues-44
fix: misc issues
2024-10-23 10:55:42 +05:30
Frappe PR Bot af0cc7126b chore(release): Bumped to Version 2.9.0 2024-10-23 05:09:14 +00:00
Jannat Patel a085050d27 build: removed frappe-ui package 2024-10-23 10:36:26 +05:30
Jannat Patel 2442f35f56 fix: added is_instructor to jinja 2024-10-23 10:35:26 +05:30
Jannat Patel ed79ea536b Merge pull request #1072 from frappe/pot_develop_2024-10-18
chore: update POT file
2024-10-18 23:06:49 +05:30
frappe-pr-bot b3d0aecd14 chore: update POT file 2024-10-18 16:04:26 +00:00
Jannat Patel 5f43e67c0b Merge pull request #1068 from pateljannat/payment-issues
fix: batch enrollment after payment completion
2024-10-17 10:39:46 +05:30
Jannat Patel 49a765a9a6 style: fix spacing 2024-10-17 10:31:56 +05:30
Jannat Patel 4d82bc86e8 style: fix spacing 2024-10-17 10:30:06 +05:30
Jannat Patel 8fe02b83b8 fix: batch enrollment after payment completion 2024-10-17 09:27:24 +05:30
Jannat Patel 9c9075606b Merge pull request #1059 from frappe/pot_develop_2024-10-11
chore: update POT file
2024-10-15 19:38:24 +05:30
Jannat Patel 53285a0d19 fix: misc issues 2024-10-14 19:17:32 +05:30
Jannat Patel 9cdeaebb47 Merge pull request #1062 from pateljannat/quiz-timer
feat: timer in quiz
2024-10-14 16:11:55 +05:30
Jannat Patel a9cb52c68b fix: hide timer instructions if duration is not set 2024-10-14 15:49:27 +05:30
Jannat Patel f33e950e83 feat: timer in quiz 2024-10-14 14:31:26 +05:30
Jannat Patel 9c9b5963fe Merge pull request #1060 from pateljannat/issues-43
fix: redirect to login before enrollment
2024-10-11 22:33:52 +05:30
Jannat Patel 1597054cc9 fix: redirect to login before enrollment 2024-10-11 22:18:18 +05:30
frappe-pr-bot deba6aa845 chore: update POT file 2024-10-11 16:04:13 +00:00
Jannat Patel 2d8ba3b84e Merge pull request #1058 from pateljannat/issues-42
fix: batch self enrollment
2024-10-11 19:22:50 +05:30
Jannat Patel e56b28abad chore: removed unnecessary lines 2024-10-11 19:17:56 +05:30
Jannat Patel eb350c5a20 fix: batch self enrollment 2024-10-11 19:16:40 +05:30
Jannat Patel 961d5ec77b Merge pull request #1057 from pateljannat/settings-minor-changes
fix: misc ux issues
2024-10-11 16:18:19 +05:30
Jannat Patel fa566514aa fix: image fetch for settings 2024-10-11 15:32:41 +05:30
Jannat Patel 6e97449bf7 fix: misc ux issues 2024-10-11 13:39:30 +05:30
Jannat Patel 016dafb3c3 Merge pull request #1056 from pateljannat/issues-41
fix: misc issues
2024-10-10 16:43:59 +05:30
Jannat Patel 675bcc8956 test: replaced FrappeTestCase with UnitTestCase 2024-10-10 16:20:53 +05:30
Jannat Patel aba4c034fc fix: misc issues 2024-10-10 14:48:59 +05:30
Jannat Patel c76d8c582f Merge pull request #1052 from pateljannat/issues-40
fix: misc quiz issues
2024-10-09 19:17:01 +05:30
Jannat Patel f1cb0e6f3c fix: usd conversion 2024-10-09 19:07:25 +05:30
Jannat Patel d296687456 fix: misc quiz issues 2024-10-09 16:03:56 +05:30
Jannat Patel 5b68001c94 Merge pull request #1049 from pateljannat/issues-39
fix: create order for razorpay
2024-10-09 11:59:57 +05:30
Frappe PR Bot 736d79b8c9 chore(release): Bumped to Version 2.8.0 2024-10-09 06:04:56 +00:00
Jannat Patel 98c0bd5f3e Merge pull request #1042 from frappe/pot_develop_2024-10-04
chore: update POT file
2024-10-09 11:34:01 +05:30
Jannat Patel 8b1d9bb5a9 fix: create order for razorpay 2024-10-09 11:31:31 +05:30
Jannat Patel 289a0f9122 Merge pull request #1046 from pateljannat/issues-38
fix: quiz columns
2024-10-08 16:27:04 +05:30
Jannat Patel 3cd08c80c8 fix: reduced with of marks column 2024-10-08 16:09:21 +05:30
Jannat Patel 3d82c36250 fix: quiz columns 2024-10-08 16:03:37 +05:30
Jannat Patel 9b9af0215a Merge pull request #1045 from pateljannat/issues-37
fix: using google docs viewer to render pdf
2024-10-08 12:37:19 +05:30
Jannat Patel 2e4cf02737 fix: using google docs viewer to render pdf 2024-10-08 12:00:51 +05:30
Jannat Patel 438e9e1c47 Merge pull request #1044 from pateljannat/open-ended-questions
feat: open ended questions
2024-10-08 10:37:44 +05:30
Jannat Patel 36ded70eef fix: only allow instructor and moderator on submission page 2024-10-08 10:21:45 +05:30
Jannat Patel ba78a15a1f fix: ui test button label 2024-10-08 10:14:04 +05:30
Jannat Patel 93061194bb fix: error toast when saving marks 2024-10-08 10:13:07 +05:30
Jannat Patel 6d41e4e552 feat: open ended questions 2024-10-07 21:18:42 +05:30
frappe-pr-bot 3b06968d0a chore: update POT file 2024-10-04 16:04:32 +00:00
Frappe PR Bot fc81f1aa26 chore(release): Bumped to Version 2.7.0 2024-10-02 06:53:07 +00:00
Jannat Patel 59d8848125 Merge pull request #1035 from pateljannat/payments
feat: payments app integration
2024-10-02 12:22:00 +05:30
Jannat Patel a067695f71 fix: removed help article from course lesson 2024-10-01 15:43:45 +05:30
Jannat Patel be870e8145 fix: payment gateway fields 2024-10-01 15:17:17 +05:30
Jannat Patel 8a17dca351 fix: minor ui changes 2024-10-01 10:43:37 +05:30
Jannat Patel 1c9f636ad1 Merge pull request #1032 from frappe/pot_develop_2024-09-27
chore: update POT file
2024-10-01 09:42:57 +05:30
Jannat Patel 008cc66cdd chore: refactor payment settings 2024-09-30 18:30:53 +05:30
Jannat Patel b6bf9c0032 Merge branch 'develop' of https://github.com/frappe/lms into payments 2024-09-30 10:16:52 +05:30
Jannat Patel d295898674 Merge pull request #1033 from pateljannat/issues-35
fix: misc UI fixes
2024-09-27 22:15:43 +05:30
frappe-pr-bot 4fdca4691a chore: update POT file 2024-09-27 16:04:07 +00:00
Jannat Patel 7c055af496 fix: telemetry capture issue 2024-09-27 21:32:46 +05:30
Jannat Patel 60a3da283e refactor: billing page ui 2024-09-27 14:14:03 +05:30
Jannat Patel 576258ec6e fix: pass options to setting fields 2024-09-27 07:05:16 +05:30
Jannat Patel 01120fbc48 chore: resolved conflicts 2024-09-27 06:24:12 +05:30
Jannat Patel ad07f883b5 fix: misc UI fixes 2024-09-27 06:19:38 +05:30
Jannat Patel bb9b179e05 Merge pull request #1031 from pateljannat/brand-settings
feat:  brand settings
2024-09-26 14:01:19 +05:30
Jannat Patel 11a9bff57d fix: dirty form for branding section 2024-09-26 12:58:00 +05:30
Jannat Patel e18f0c9dad feat: brand settings 2024-09-26 12:09:58 +05:30
Jannat Patel 41ad3d00de Merge pull request #1030 from pateljannat/fix-evaluation-issue
fix: evaluation error message issue
2024-09-25 11:29:42 +05:30
Frappe PR Bot b74c1670ca chore(release): Bumped to Version 2.6.0 2024-09-25 05:47:45 +00:00
Jannat Patel 33c76e842f fix: evaluation error message issue 2024-09-25 11:10:26 +05:30
Jannat Patel 35a7cce283 feat: payment gateway settings 2024-09-25 10:50:53 +05:30
Jannat Patel e0f569c382 feat: payment flow with payments app 2024-09-24 18:14:34 +05:30
Jannat Patel d8ab88be28 Merge branch 'develop' of https://github.com/frappe/lms into payments 2024-09-24 14:14:49 +05:30
Jannat Patel 04552bdef6 Merge pull request #1025 from pateljannat/categories-in-courses
feat: course categories
2024-09-24 12:55:41 +05:30
Jannat Patel ad5bf89b35 test: enter instructor value 2024-09-24 12:32:07 +05:30
Jannat Patel 88b38dfd83 test: category test in course form in UI 2024-09-24 12:22:34 +05:30
Jannat Patel 75e9ca395f chore: removed unnecessary custom fields 2024-09-24 10:59:27 +05:30
Jannat Patel 6fb206cc4e feat: updating category from settings 2024-09-24 10:33:23 +05:30
Jannat Patel 62cb198492 Merge branch 'develop' of https://github.com/frappe/lms into categories-in-courses 2024-09-23 19:23:49 +05:30
Jannat Patel 9609329f01 Merge pull request #1028 from pateljannat/fix-signup-customisations
fix: signup conditions
2024-09-23 19:12:29 +05:30
Jannat Patel c93808af94 fix: signup conditions 2024-09-23 18:41:35 +05:30
Jannat Patel 58866260ec Merge branch 'develop' of https://github.com/frappe/lms into categories-in-courses 2024-09-23 18:39:19 +05:30
Jannat Patel e6157ff411 Merge pull request #1027 from pateljannat/refactor-signup-customisations
refactor: signup customisations
2024-09-23 18:23:04 +05:30
Jannat Patel 8cca8920ee chore: removed unnecessary file 2024-09-23 18:07:08 +05:30
Jannat Patel ab039dbd46 refactor: signup customisations 2024-09-23 18:04:36 +05:30
Jannat Patel 9853ab3fd9 Merge pull request #1024 from frappe/pot_develop_2024-09-20
chore: update POT file
2024-09-23 16:11:57 +05:30
Jannat Patel dc2bf9f13e feat: category settings 2024-09-23 16:11:17 +05:30
Jannat Patel 7c90ca4040 feat: category in settings 2024-09-20 22:15:59 +05:30
frappe-pr-bot 75a90e1f39 chore: update POT file 2024-09-20 16:04:14 +00:00
Jannat Patel bc4b17cc3d Merge pull request #1022 from pateljannat/evaluator_name_issue
fix: evaluator name issue
2024-09-19 15:18:05 +05:30
Jannat Patel 8c454a333e fix: evaluator name issue 2024-09-19 14:19:55 +05:30
Jannat Patel cef4b70182 feat: payment through payments app 2024-09-19 12:46:56 +05:30
Jannat Patel 3cda563583 fix: padding of settings modal 2024-09-18 14:46:29 +05:30
Jannat Patel 545326a02a Merge pull request #1021 from pateljannat/fix-help-video-url
fix: url for lesson help videos
2024-09-18 14:42:33 +05:30
Jannat Patel 14ce5d7e23 fix: url for lesson help videos 2024-09-18 11:49:38 +05:30
Frappe PR Bot b6422d1046 chore(release): Bumped to Version 2.5.0 2024-09-18 04:46:24 +00:00
Jannat Patel 7196bbe221 Merge pull request #1019 from pateljannat/issues-34
fix: misc issues
2024-09-18 09:03:33 +05:30
Jannat Patel bed16c3726 fix: condition to show course addition button 2024-09-18 08:56:35 +05:30
Jannat Patel d18ca232e3 fix: misc issues 2024-09-18 08:43:16 +05:30
Jannat Patel d1200d0fa9 fix: profile page empty bio error 2024-09-17 11:48:49 +05:30
Jannat Patel d1c88b306f Merge pull request #1018 from pateljannat/batch-quiz
feat: quiz page
2024-09-17 10:42:34 +05:30
Jannat Patel 7f2723f9cb Merge pull request #1014 from pateljannat/batch-welcome-email-fix
fix: batch confirmation email trigger
2024-09-17 10:30:00 +05:30
Jannat Patel 8df4bef71a feat: quiz page 2024-09-17 10:25:59 +05:30
Jannat Patel aa87622606 Merge pull request #1017 from pateljannat/refactor-lesson-editor
refactor: adding quiz and uploading content to a lesson
2024-09-16 19:19:42 +05:30
Jannat Patel b91339fe28 fix: removed unnecessary files 2024-09-16 18:50:06 +05:30
Jannat Patel 17d4973ab8 test: fixed course creation 2024-09-16 18:41:25 +05:30
Jannat Patel 3c12548420 Merge pull request #1016 from frappe/pot_develop_2024-09-13
chore: update POT file
2024-09-16 16:50:16 +05:30
Jannat Patel 20c10f1645 feat: help guide videos 2024-09-16 16:49:17 +05:30
Jannat Patel a7843e0e3a refactor: uploading content in lesson 2024-09-16 10:33:16 +05:30
frappe-pr-bot 169ea4385f chore: update POT file 2024-09-13 16:04:09 +00:00
Jannat Patel 9549f3a3ed Merge pull request #1015 from pateljannat/course-completion-funnel
chore: analytics for course completion
2024-09-13 11:37:22 +05:30
Jannat Patel ba66c2549f chore: renamed lesson_progress to course_progress for analytics 2024-09-13 11:21:59 +05:30
Jannat Patel 76c3e630cc chore: analytics for course completion 2024-09-13 11:18:17 +05:30
Jannat Patel 7a0b952638 style: trigger condition 2024-09-13 09:57:41 +05:30
Jannat Patel 5966a3edad fix: batch confirmation email trigger 2024-09-13 09:57:06 +05:30
Jannat Patel d44c7cd9fc Merge pull request #1010 from pateljannat/evaluation-calendar
feat: Evaluation and Certification from Learning Portal
2024-09-12 16:48:51 +05:30
Jannat Patel 46553987ac fix: certification tab title 2024-09-12 16:22:43 +05:30
Jannat Patel 45725f1f6e chore: bumped up frappe-ui 2024-09-12 14:31:55 +05:30
Jannat Patel 58369ba65e fix: evaluator info and other styles 2024-09-11 19:51:14 +05:30
Jannat Patel 5ce67dda2e Merge pull request #1002 from prachi8848/cusrsor
feat: added a cusrsor
2024-09-11 15:05:48 +05:30
Jannat Patel 237ff8db07 Merge pull request #1008 from frappe/pot_develop_2024-09-06
chore: update POT file
2024-09-10 21:02:55 +05:30
Jannat Patel 7da608ed44 feat: certification details and form 2024-09-10 21:00:49 +05:30
Jannat Patel 60f2e86b42 feat: evaluation feedback record 2024-09-09 20:05:08 +05:30
frappe-pr-bot b5e67a25d2 chore: update POT file 2024-09-06 16:03:59 +00:00
Jannat Patel 9d2ef4929c Merge pull request #1007 from pateljannat/fix-notification
fix: certificate request creation email
2024-09-06 10:49:52 +05:30
Jannat Patel 050084e552 style: fix formatting 2024-09-06 10:40:25 +05:30
Jannat Patel 86e9739218 fix: certificate request creation email 2024-09-06 10:19:25 +05:30
Jannat Patel bd94890da7 Merge pull request #1005 from pateljannat/minor-ui-fix
fix: member-list ui
2024-09-05 20:36:41 +05:30
Jannat Patel 965f6adb90 style: fixed formatting 2024-09-05 20:31:22 +05:30
Jannat Patel 4979569cf3 fix: member-list ui 2024-09-05 19:59:44 +05:30
Frappe PR Bot 5c21a0532a chore(release): Bumped to Version 2.4.0 2024-09-04 05:00:25 +00:00
sonali8848 a2025c0571 feat: added a cusrsor 2024-09-02 09:41:22 +00:00
Jannat Patel e07aae3fb0 Merge pull request #997 from pateljannat/issues-33
fix: slides rendering issue
2024-08-29 19:26:08 +05:30
Jannat Patel 65d628ffc0 fix: slides rendering issue 2024-08-29 11:10:43 +05:30
Jannat Patel bf290bbf0a Merge pull request #994 from akhilnarang/fix-user-creation
fix(overrides): call parent's `after_insert()` as well
2024-08-27 14:56:11 +05:30
Akhil Narang 3c9059025b fix(overrides): call parent's after_insert() as well
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2024-08-27 14:08:35 +05:30
Jannat Patel 4b0413720b Merge pull request #993 from pateljannat/quiz-submission-issue
fix: quiz submission report issue
2024-08-27 11:58:14 +05:30
Jannat Patel f8b4ff4bd3 fix: quiz submission report issue 2024-08-27 10:46:06 +05:30
Jannat Patel 3b8ff171f4 Merge pull request #989 from frappe/pot_develop_2024-08-23
chore: update POT file
2024-08-26 14:45:59 +05:30
frappe-pr-bot dec270a10b chore: update POT file 2024-08-23 16:04:00 +00:00
Jannat Patel 152a339c4e Merge pull request #986 from pateljannat/app-switcher
feat: App switcher
2024-08-23 12:40:16 +05:30
Jannat Patel 395fe700e0 fix: removed switch to desk 2024-08-23 12:22:11 +05:30
Jannat Patel ec25e895dc feat: app switcher 2024-08-23 12:21:22 +05:30
Frappe PR Bot e02e4c7ab4 chore(release): Bumped to Version 2.3.0 2024-08-21 05:20:24 +00:00
Jannat Patel e69cc9af1a Merge pull request #980 from pateljannat/member-addition
feat: Add users from the portal
2024-08-19 14:11:59 +05:30
Jannat Patel 98b8464e1a fix: ui test 2024-08-19 13:07:48 +05:30
Jannat Patel 0170fcc111 Merge pull request #968 from frappe/pot_develop_2024-08-16
chore: update POT file
2024-08-19 13:04:21 +05:30
Jannat Patel 0be5439e81 fix: tests 2024-08-19 12:31:46 +05:30
Jannat Patel 63f857b8fc fix: linters 2024-08-19 12:12:51 +05:30
Jannat Patel a3b8ed8f91 fix: documented the api 2024-08-19 12:03:32 +05:30
Jannat Patel cdd46667f3 feat: add new member 2024-08-19 11:47:17 +05:30
frappe-pr-bot 2f8acea988 chore: update POT file 2024-08-16 16:04:13 +00:00
Jannat Patel 75f0e5b9f1 feat: search member 2024-08-16 20:59:51 +05:30
Jannat Patel ce51129e84 feat: member list 2024-08-16 11:26:11 +05:30
Jannat Patel 86aa8b0a2a Merge pull request #967 from pateljannat/issues-32
fix: settings ui
2024-08-14 12:47:31 +05:30
Jannat Patel aeae62a45c chore: linters 2024-08-14 12:35:56 +05:30
Jannat Patel 6b12df44a0 fix: settings ui 2024-08-14 12:12:13 +05:30
Frappe PR Bot a710183bc7 chore(release): Bumped to Version 2.2.0 2024-08-14 05:57:31 +00:00
Jannat Patel 669316ba14 Merge pull request #965 from pateljannat/make-release
ci: automated release PR
2024-08-14 11:08:26 +05:30
Jannat Patel 6c18f9a02f ci: automated release PR 2024-08-14 10:44:11 +05:30
Jannat Patel 363edb9a50 Merge pull request #964 from pateljannat/settings
feat: Settings
2024-08-13 19:14:07 +05:30
Jannat Patel afbf64170a fix: removed old settings 2024-08-13 19:03:17 +05:30
Jannat Patel 14f36d0c64 chore: removed unnecessary file 2024-08-13 18:59:39 +05:30
Jannat Patel ceecab395b feat: settings 2024-08-13 18:53:27 +05:30
Jannat Patel b8eb9fd717 Merge branch 'develop' of https://github.com/frappe/lms into settings 2024-08-13 09:42:17 +05:30
Jannat Patel 230a52f06b Merge pull request #963 from pateljannat/issues-31
fix: misc issues
2024-08-13 09:16:12 +05:30
Jannat Patel 3e82608d5f chore: fixed linters 2024-08-12 20:13:57 +05:30
Jannat Patel cf2c2345c3 fix: discussions text 2024-08-12 20:10:10 +05:30
Jannat Patel 05ebe4b787 fix: lesson structure issue 2024-08-12 20:09:56 +05:30
Frappe PR Bot a744a43d14 chore(release): Bumped to Version 2.1.0 2024-08-12 12:32:42 +00:00
Jannat Patel 5abdbfec1f Merge pull request #962 from pateljannat/posthog
chore: product analytics
2024-08-12 18:01:45 +05:30
Jannat Patel 0335b3b4d0 chore: fixed linters 2024-08-12 17:38:59 +05:30
Jannat Patel 703fafd6c3 chore: analytics 2024-08-12 17:13:31 +05:30
Jannat Patel b956c4e383 Merge branch 'develop' of https://github.com/frappe/lms into posthog 2024-08-12 14:17:04 +05:30
Jannat Patel d0d1fb2c8c Merge pull request #944 from pateljannat/quiz-creation
Quiz creation
2024-08-12 14:13:23 +05:30
Jannat Patel d18a6f6e73 chore: removed workspaces from package.json 2024-08-12 13:13:21 +05:30
Jannat Patel 2994144718 feat: quiz creation from lesson form 2024-08-12 12:47:17 +05:30
Jannat Patel 62ab853605 feat: quiz creation from lesson form 2024-08-12 12:46:38 +05:30
Jannat Patel 7f7986d77a chore: fixed linters 2024-08-12 10:04:42 +05:30
Jannat Patel 61f01cc51b chore: resolved conflicts 2024-08-12 10:02:22 +05:30
Jannat Patel 86af8c6301 Merge pull request #960 from mohsinalimat/patch-1
fix: lms setting to lms settings
2024-08-09 22:21:35 +05:30
Jannat Patel f1b0fcfbfc Merge pull request #961 from frappe/pot_develop_2024-08-09
chore: update POT file
2024-08-09 21:50:21 +05:30
frappe-pr-bot ab5ce39645 chore: update POT file 2024-08-09 16:04:06 +00:00
Jannat Patel 685e09ce4b feat: question update 2024-08-09 20:38:14 +05:30
MohsinAli 8ed4f775e5 fix: lms setting to lms settings 2024-08-09 13:58:50 +05:30
Jannat Patel a3a3085b1f fix: update question title in quiz 2024-08-08 22:16:21 +05:30
Jannat Patel ed97640107 feat: allow updating questions 2024-08-07 18:15:50 +05:30
Jannat Patel a9e93a679b feat: posthog initialization 2024-08-07 11:31:58 +05:30
Jannat Patel 418c36c09f Merge pull request #957 from frappe/pot_develop_2024-08-06
chore: update POT file
2024-08-06 12:17:49 +05:30
frappe-pr-bot 935f7f1f7b chore: update POT file 2024-08-06 06:32:08 +00:00
Jannat Patel 9a0056b6ca Merge branch 'develop' of https://github.com/frappe/lms into develop 2024-08-06 11:50:23 +05:30
Jannat Patel cd56da5d85 fix: changed upstream command for pot file generation 2024-08-06 11:47:37 +05:30
Jannat Patel 97d5d853fc Merge pull request #955 from pateljannat/issues-30
fix: lesson auto save
2024-08-06 11:11:50 +05:30
Jannat Patel 8adfe247b2 fix: lesson auto save 2024-08-05 18:14:35 +05:30
Jannat Patel afe7df2989 fix: fetch question 2024-08-05 16:29:43 +05:30
Jannat Patel cdb028c69c feat: settings 2024-08-05 15:12:45 +05:30
Jannat Patel eed330662b Merge pull request #953 from pateljannat/issues-29
fix: course cards home page rendering
2024-08-05 14:20:36 +05:30
Jannat Patel 26db10bbe0 fix: course cards home page rendering 2024-08-05 14:04:08 +05:30
Jannat Patel 14230bd588 Merge pull request #951 from pateljannat/issues-28
fix: spacing and widths
2024-08-05 11:32:12 +05:30
Jannat Patel 699c821edd fix: spacing and widths 2024-08-05 11:12:34 +05:30
Jannat Patel 27ca13ece6 feat: add questions to quiz 2024-08-02 20:20:43 +05:30
Jannat Patel 6820dfc820 fix: make question attachments public 2024-08-01 13:02:20 +05:30
Jannat Patel 471e7d9229 fix: fetch question details 2024-08-01 13:00:37 +05:30
Jannat Patel e0855a2c1b fix: quiz question population issue 2024-07-31 22:30:29 +05:30
Jannat Patel 6a0b37a4d4 chore: changed release branch to develop 2024-07-31 12:43:04 +05:30
Jannat Patel f7fd6916e2 chore: release notes action 2024-07-31 12:15:24 +05:30
Jannat Patel 30e61f4b7c Merge pull request #947 from pateljannat/semantic-release
chore: semantic release action
2024-07-31 11:45:42 +05:30
Jannat Patel 48b37d58d8 chore: semantic release action 2024-07-31 11:10:47 +05:30
Jannat Patel b8c3bdc0b4 feat: open modal to edit question 2024-07-31 11:07:46 +05:30
Jannat Patel e96f18df7c Merge pull request #946 from pateljannat/issues-27
fix: quiz shuffle issue
2024-07-30 18:04:26 +05:30
Jannat Patel 7d15527831 fix: quiz shuffle issue 2024-07-30 16:58:09 +05:30
Jannat Patel 794c0e760b Merge pull request #945 from pateljannat/issues-26
fix: eval request issues
2024-07-30 15:26:46 +05:30
Jannat Patel e46a60d00a chore: linters 2024-07-30 15:05:24 +05:30
Jannat Patel 819aac70fd fix: linters 2024-07-30 14:54:21 +05:30
Jannat Patel ed7db2d7c5 fix: eval request issues 2024-07-30 14:17:17 +05:30
Jannat Patel a450c846a6 feat: questions table 2024-07-29 19:44:04 +05:30
Jannat Patel fa774b0db2 feat: quiz creation 2024-07-19 20:05:28 +05:30
Jannat Patel 98a56f9117 Merge pull request #936 from pateljannat/image-pasting
fix: batch filters on desk
2024-07-19 12:19:34 +05:30
Jannat Patel cbc4b8c59d chore: removed unused plugin 2024-07-19 12:07:35 +05:30
Jannat Patel 69d266e018 feat: image pasting 2024-07-19 11:55:36 +05:30
Jannat Patel 4bc3ac1665 fix: batch filters on desk 2024-07-18 21:29:40 +05:30
Jannat Patel e0de9d70de Merge pull request #933 from pateljannat/issues-25
fix: misc issues
2024-07-17 11:40:35 +05:30
Jannat Patel 493bab8163 fix: multiline tags 2024-07-17 11:29:14 +05:30
Jannat Patel 25a2d82e82 fix: misc issues 2024-07-16 16:11:24 +05:30
Jannat Patel 0183677494 Merge pull request #932 from pateljannat/batch-categories
feat: batch categories filter
2024-07-15 14:45:57 +05:30
Jannat Patel 7ae9244896 feat: batch categories filter 2024-07-15 13:45:38 +05:30
Jannat Patel 15330cb41d Merge pull request #928 from pateljannat/completion-certificate
feat: completion certificate
2024-07-12 20:36:06 +05:30
Jannat Patel 166996d77a chore: removed unnecessary lines 2024-07-12 20:17:42 +05:30
Jannat Patel 4943e0e902 chore: fixed linters 2024-07-12 19:58:52 +05:30
Jannat Patel 1db6a8bfda chore: fixed linters 2024-07-12 19:56:01 +05:30
Jannat Patel 57f43b256a feat: enable certification from course form 2024-07-12 19:46:45 +05:30
Jannat Patel 23b2e8d682 feat: generate certificate from course page 2024-07-12 15:56:50 +05:30
Jannat Patel 6e1d62340f feat: completion certificate 2024-07-11 18:12:15 +05:30
Jannat Patel 63d613a88e chore: fixed linters 2024-07-10 22:58:22 +05:30
Jannat Patel 70a4d16a8a chore: merge conflict 2024-07-10 22:40:40 +05:30
Jannat Patel 9960507318 Merge pull request #924 from pateljannat/reorder-lessons
feat: reorder lessons
2024-07-10 22:00:54 +05:30
Jannat Patel a84b225247 chore: fixed linters 2024-07-10 21:53:49 +05:30
Jannat Patel a1f938eaaf chore: removed unnecessary lines 2024-07-10 21:51:00 +05:30
Jannat Patel f7027e9cfd fix: success message after lesson moved 2024-07-10 21:38:21 +05:30
Jannat Patel 8164526763 fix: misc issues 2024-07-10 16:13:28 +05:30
Jannat Patel 2ecc07ee58 feat: reorder lessons 2024-07-09 22:45:45 +05:30
Jannat Patel 8edfe041c3 Merge pull request #922 from pateljannat/delete-lesson
feat: delete lessons
2024-07-08 19:53:19 +05:30
Jannat Patel e3e54b0188 feat: delete lessons 2024-07-08 18:15:22 +05:30
Jannat Patel 602d457212 Merge pull request #921 from pateljannat/course-cache-issue
fix: course cache issue
2024-07-08 12:38:02 +05:30
Jannat Patel 8bd4a5448b fix: achievements title issue 2024-07-08 11:18:59 +05:30
Jannat Patel 2257c09228 fix: course cache issue 2024-07-05 19:24:49 +05:30
Jannat Patel dac8a3ecf2 fix: translations upstream 2024-07-05 16:03:36 +05:30
Jannat Patel 288f85b2f3 fix: indian state validation 2024-07-04 17:35:44 +05:30
Jannat Patel d5d9e5e6e8 chore: changed translation cron 2024-07-04 17:32:10 +05:30
Jannat Patel e194f2efea Merge pull request #919 from pateljannat/quiz-instructions
fix: instructions for user input questions
2024-07-04 10:25:00 +05:30
Jannat Patel 686839adc1 fix: instructions for user input questions 2024-07-03 22:41:39 +05:30
Jannat Patel 0c52b5a8ec fix: badge share url 2024-07-02 16:17:28 +05:30
Jannat Patel f80a139c93 Merge pull request #913 from pateljannat/translations
feat: translations via gettext
2024-07-02 14:54:01 +05:30
Jannat Patel eeadd6910e fix: changed cron to wednesday 2024-07-02 14:43:08 +05:30
Jannat Patel eed16d9604 feat: translations via gettext 2024-07-01 11:15:12 +05:30
Jannat Patel 3745db6da4 Merge pull request #905 from pateljannat/search-course
feat: search courses
2024-06-28 19:05:34 +05:30
Jannat Patel 4ef694a2ed fix: search cache issue 2024-06-28 18:51:47 +05:30
Jannat Patel 279bb89ca9 feat: search courses 2024-06-28 15:58:35 +05:30
Jannat Patel 0bc5714392 Merge pull request #902 from pateljannat/quiz-limit
feat: limit questions in quiz
2024-06-28 12:48:10 +05:30
Jannat Patel 764f358708 feat: limit questions in quiz 2024-06-28 12:21:07 +05:30
Jannat Patel bf43fd5079 Merge pull request #901 from pateljannat/issues-24
fix: lesson editor menu position
2024-06-27 21:58:39 +05:30
Jannat Patel 6adc62c72a fix: lesson editor menu position 2024-06-27 21:50:02 +05:30
Jannat Patel 2cf894df59 Merge pull request #900 from pateljannat/correct-workspace-links
fix: corrected workspace links
2024-06-27 21:29:08 +05:30
Jannat Patel 72053dbf56 fix: corrected workspace links 2024-06-27 21:20:14 +05:30
Jannat Patel 0cd50ff1b6 Merge pull request #898 from pateljannat/google-drive
feat: more embed sources for lesson components
2024-06-27 14:28:48 +05:30
Jannat Patel d3f443014c feat: more embed sources for lesson components 2024-06-27 12:30:48 +05:30
Jannat Patel ecd56609a0 Merge pull request #895 from pateljannat/issues-23
fix: minor UI issues
2024-06-26 15:01:06 +05:30
Jannat Patel 1af10b7f96 chore: removed unused imports 2024-06-26 14:29:29 +05:30
Jannat Patel 55170361c1 fix: minor UI issues 2024-06-26 14:25:16 +05:30
Jannat Patel daa42f146d Merge pull request #894 from pateljannat/issues-22
fix: batch ui
2024-06-26 13:52:30 +05:30
Jannat Patel 67ebd30836 fix: batch ui 2024-06-26 13:10:20 +05:30
Jannat Patel ac282cebfb Merge pull request #878 from MuhammadKazimSadiq/recent-courses-web-template
Recent courses web template
2024-06-26 11:13:38 +05:30
Jannat Patel 4aea074041 Merge pull request #888 from pateljannat/batch-instructors
feat: instructors in courses and batches
2024-06-26 10:42:01 +05:30
Jannat Patel 888ea5a911 test: type instructor 2024-06-26 10:32:42 +05:30
Jannat Patel f02b9c09e6 chore: fixed merge conflicts 2024-06-26 09:34:09 +05:30
Jannat Patel 5e91553190 test: instructor when creating course 2024-06-26 09:33:03 +05:30
Md Hussain Nagaria 326c77cdb9 fix: clip progress bar width to 100% (#892)
* fix: course progress bar should not go beyond 100% width

* refactor: progress bar component should include outer body div
2024-06-26 00:29:55 +05:30
Muhammad Kazim e10af413b2 Merge branch 'develop' into recent-courses-web-template 2024-06-25 19:44:08 +03:30
Md Hussain Nagaria d4a15ade98 fix: seats left condition (#889) 2024-06-25 21:28:32 +05:30
Jannat Patel b18a3cb5e1 test: instructor for courses 2024-06-25 19:56:50 +05:30
Jannat Patel 96028c9f42 feat: instructors in courses and batches 2024-06-25 18:43:28 +05:30
Jannat Patel 8625ac048a Merge pull request #877 from nikkothari22/fix-seats-badge
fix: show "Seats Left" instead of "Seat Left" if more than 1 seat is available
2024-06-24 14:35:11 +05:30
Jannat Patel 30934f9eba Merge pull request #884 from pateljannat/issues-21
fix: job page layout
2024-06-24 14:31:22 +05:30
Jannat Patel 8349f47cbe Merge branch 'develop' into fix-seats-badge 2024-06-24 14:30:24 +05:30
Muhammad Kazim 9d3d93443f fix linting issues with pre-commit 2024-06-22 10:46:06 +00:00
Muhammad Kazim 4b6d1c296c Merge branch 'frappe:develop' into recent-courses-web-template 2024-06-22 13:28:51 +03:30
Jannat Patel 0a3a48759f fix: deleted fixture json 2024-06-21 19:23:18 +05:30
Jannat Patel 407bea4ab9 fix: removed badge fixture 2024-06-21 19:17:23 +05:30
Jannat Patel 3ba34f36eb fix: removed meta from eval web form as its not needed 2024-06-21 19:03:22 +05:30
Jannat Patel 8d1c03d4c1 chore: removed unused imports 2024-06-21 18:52:12 +05:30
Jannat Patel ed739b25e2 fix: job page layout 2024-06-21 18:41:10 +05:30
Nikhil Kothari 807c9b2225 fix: use single span instead of multiple badges 2024-06-21 17:17:36 +05:30
Jannat Patel 7d3dc8df90 Merge pull request #883 from pateljannat/batch-timezone
feat: timezone in batches
2024-06-21 14:23:59 +05:30
Jannat Patel aafb8948d2 chore: resolved conflicts 2024-06-21 14:05:03 +05:30
Jannat Patel 4f2dd7654c fix: notification template for timezone 2024-06-21 13:17:54 +05:30
Jannat Patel 9c2bebb3d9 feat: timezone in batches 2024-06-21 13:08:58 +05:30
Jannat Patel e93e82b56c Merge pull request #882 from pateljannat/video-player
feat: video and audio player
2024-06-21 11:44:08 +05:30
Jannat Patel f783b981e5 feat: added support to embed aparat 2024-06-21 11:38:30 +05:30
Jannat Patel b5a904354a test: fix lesson content 2024-06-21 11:24:28 +05:30
Jannat Patel ed7c30057c test: increase wait time when lesson is opened 2024-06-21 10:36:26 +05:30
Jannat Patel 05e8513ad1 feat: video player 2024-06-20 16:46:50 +05:30
Muhammad Kazim 775f8db31e Merge branch 'frappe:develop' into recent-courses-web-template 2024-06-17 00:57:16 +03:30
Muhammad Kazim 4b6d3fe968 web template for recently published courses 2024-06-16 21:26:16 +00:00
Nikhil Kothari e397295b5e fix: use else-if 2024-06-14 16:30:33 +05:30
Nikhil Kothari 1d16c46003 fix: show "30 seats left" instead of "seat left" in batches 2024-06-14 16:29:58 +05:30
Jannat Patel 3ba8805413 Merge branch 'develop' of https://github.com/frappe/lms into video-player 2024-06-14 10:28:22 +05:30
Jannat Patel 63f4dc0caa Merge pull request #876 from pateljannat/issues-20
fix: misc issues
2024-06-14 10:22:16 +05:30
Jannat Patel 965bdd7890 fix: hide discussions is lesson has quiz 2024-06-14 09:48:22 +05:30
Jannat Patel a99c41a07b fix: misc changes 2024-06-13 15:34:57 +05:30
Jannat Patel 81feee887c Merge pull request #870 from pateljannat/issues-19
fix: better validation message on course creation
2024-06-12 12:11:47 +05:30
Jannat Patel 77433ebb7c fix: better validation message on course creation 2024-06-12 11:44:14 +05:30
Jannat Patel d5614322c5 Merge pull request #869 from pateljannat/issues-18
fix: event meeting generation
2024-06-12 10:54:16 +05:30
Jannat Patel 479ff037c6 fix: convert eval string to dict 2024-06-12 10:42:47 +05:30
Jannat Patel 8a74f495e7 fix: event meeting generation 2024-06-12 10:32:57 +05:30
Jannat Patel 231f2cbc14 feat: video player 2024-06-12 10:27:33 +05:30
Jannat Patel 1f466482f8 Merge pull request #867 from pateljannat/issues-17
fix: perf issues
2024-06-07 11:52:41 +05:30
Jannat Patel 103ecef9f4 fix: misc issues 2024-06-07 11:37:46 +05:30
Jannat Patel 2744002390 Merge pull request #866 from pateljannat/issues-16
fix: misc issues
2024-06-06 22:04:47 +05:30
Jannat Patel fd26d2bcd1 fix: misc issues 2024-06-06 21:57:24 +05:30
Md Hussain Nagaria a55da8149a fix: forward resource submit promise (#861) 2024-06-06 18:23:14 +05:30
Jannat Patel 631f69bd75 Merge pull request #860 from pateljannat/issues-15
fix: sold out batch issue
2024-06-06 13:07:13 +05:30
Jannat Patel 621556263b fix: sold out batch issue 2024-06-06 13:00:08 +05:30
Jannat Patel 6fdd1a5f09 fix: job application count 2024-06-05 15:58:08 +05:30
Jannat Patel a2b3bc8c1f Merge pull request #856 from pateljannat/jobs-applied
feat: job application count
2024-06-05 13:56:57 +05:30
Jannat Patel d932baf896 feat: job application count 2024-06-05 10:53:31 +05:30
Jannat Patel a9b469d3bf Merge pull request #849 from pateljannat/sidebar-edit
feat: configure sidebar items
2024-06-04 09:43:35 +05:30
Jannat Patel e2bd9401a6 fix: removed unused imports 2024-06-04 09:22:16 +05:30
Jannat Patel c6ada95b9d feat: edit and delete sidebar item 2024-06-03 20:28:25 +05:30
Jannat Patel 330a2f632a feat: edit and delete sidebar item 2024-06-03 20:27:38 +05:30
Jannat Patel bf6a7a85a7 feat: web pages for sidebar 2024-05-31 21:26:11 +05:30
Jannat Patel e609153f4f Merge branch 'develop' of https://github.com/frappe/lms into sidebar-edit 2024-05-30 12:31:50 +05:30
Jannat Patel bedb1dc8d3 Merge pull request #852 from pateljannat/issues-14
fix: evaluation related issues
2024-05-30 12:24:32 +05:30
Jannat Patel 7f2821f639 fix: evaluation related issues 2024-05-30 12:16:01 +05:30
Jannat Patel 844f4b5e8d feat: new web page table 2024-05-30 11:01:30 +05:30
Jannat Patel 1e69ff7de8 feat: configure sidebar items 2024-05-29 17:16:09 +05:30
Jannat Patel e6d58721f0 Merge pull request #846 from pateljannat/share-badge
feat: share badge on linkedin
2024-05-28 12:37:51 +05:30
Jannat Patel 7c077ace95 feat: share badge on social media 2024-05-28 12:05:16 +05:30
Jannat Patel d03dd3d20d Merge branch 'develop' of https://github.com/frappe/lms into share-badge 2024-05-27 17:36:31 +05:30
Jannat Patel a780668aac Merge pull request #841 from pateljannat/featured-courses
feat: featured courses
2024-05-27 15:51:31 +05:30
Jannat Patel c0998ca8b3 feat: share badge on linkedin 2024-05-27 15:31:53 +05:30
Jannat Patel b7dd488886 feat: featured courses 2024-05-27 15:30:15 +05:30
Jannat Patel cb9125632a Merge pull request #837 from pateljannat/issues-13
fix: minor issues
2024-05-24 17:08:03 +05:30
Jannat Patel 9a776dabed fix: minor issues 2024-05-24 17:00:14 +05:30
Jannat Patel 180c8941a4 Merge pull request #787 from Bowrna/patch-2
Update README.md
2024-05-24 15:34:38 +05:30
Jannat Patel f6b83d3518 Merge branch 'develop' into patch-2 2024-05-24 15:25:53 +05:30
Jannat Patel 97712dbdc0 Merge pull request #711 from ph4ni/main
Added batch start and end date validation
2024-05-24 15:19:58 +05:30
Jannat Patel febf38a47a Merge pull request #739 from ahmadRagheb/patch-3
Update edit.html
2024-05-24 15:18:54 +05:30
Jannat Patel 753ae01efc Merge pull request #836 from pateljannat/notifications
feat: notifications
2024-05-24 15:13:39 +05:30
Jannat Patel cef638e37a chore: removed unnecessary code 2024-05-24 15:08:18 +05:30
Jannat Patel 850069d380 feat: batch notifications 2024-05-24 13:08:02 +05:30
Jannat Patel a748e2c2db feat: notification on mentions 2024-05-23 21:25:22 +05:30
Jannat Patel f38aebbc9c feat: notifications 2024-05-22 20:40:02 +05:30
Jannat Patel 8e1b871f87 Merge pull request #824 from pateljannat/issues-12
fix: edit permission and other issues
2024-05-17 18:31:09 +05:30
Jannat Patel 76ea4fc1ae fix: edit permission and other issues 2024-05-17 18:03:04 +05:30
Jannat Patel 36c7c10d94 Merge pull request #821 from pateljannat/issues-11
fix: batch related issues
2024-05-16 13:15:24 +05:30
Jannat Patel 5148fcf25b fix: batch related issues 2024-05-16 12:53:14 +05:30
Jannat Patel d00d152fdc Merge pull request #816 from pateljannat/ui-tests
test: course creation flow
2024-05-16 11:46:29 +05:30
Jannat Patel 7123736707 chore: removed unnecessary files 2024-05-16 10:56:42 +05:30
Jannat Patel 0ad685c262 text: removed user check 2024-05-15 18:10:12 +05:30
Jannat Patel f2bef08568 ci: revert linter changes 2024-05-15 18:06:35 +05:30
Jannat Patel 7651eb5f97 test: fix discussions and asset testing 2024-05-15 17:56:11 +05:30
Jannat Patel a0fc1b0a9e test: course creation flow 2024-05-15 14:55:54 +05:30
Jannat Patel a62fd757d4 test: course creation flow 2024-05-15 14:55:28 +05:30
Jannat Patel 0094809273 Merge pull request #795 from pateljannat/badges
feat: badges
2024-05-14 19:46:29 +05:30
Jannat Patel 2ea5858716 fix: reverted unnecesary custom fields 2024-05-14 15:14:41 +05:30
Jannat Patel 753e62b441 feat: auto assign badges 2024-05-14 14:53:34 +05:30
Jannat Patel 68a1d1e436 fix: progress update 2024-05-13 16:50:52 +05:30
Jannat Patel 0d89f51d78 chore: merge conflicts 2024-05-13 11:16:13 +05:30
Jannat Patel 9670dfa916 Merge pull request #810 from pateljannat/issues-10
fix: mobile and lesson issues
2024-05-13 11:14:00 +05:30
Jannat Patel 2a19fbc3d2 fix: linter github action 2024-05-13 10:39:03 +05:30
Jannat Patel e98d7d29f7 fix: removed permission check for linters 2024-05-13 10:31:09 +05:30
Jannat Patel 3c5918d485 fix: added branches to linters github action 2024-05-13 10:23:33 +05:30
Jannat Patel 65e9d164f5 fix: linter action 2024-05-13 10:22:11 +05:30
Jannat Patel 9d0b120cde fix: removed unnecessary commits 2024-05-10 19:04:13 +05:30
Jannat Patel 10ed37ec67 fix: mobile and lesson issues 2024-05-10 18:41:39 +05:30
Jannat Patel 528ab8f796 Merge pull request #808 from pateljannat/issues-9
fix: course issues
2024-05-09 16:45:43 +05:30
Jannat Patel 30973eb78d fix: course issues 2024-05-09 16:35:04 +05:30
Jannat Patel 2be7645c0c fix: course issues 2024-05-09 16:34:54 +05:30
Jannat Patel 0075c44918 fix: achievements position 2024-05-09 12:25:48 +05:30
Jannat Patel 4a321440d9 Merge pull request #803 from pateljannat/issues-8
fix: certified participants page mobile layout
2024-05-08 11:15:38 +05:30
Jannat Patel b4cc0c6807 fix: certified participants page mobile layout 2024-05-07 19:03:27 +05:30
Jannat Patel 98c748359a Merge pull request #802 from pateljannat/issues-7
fix: misc issues
2024-05-06 16:44:28 +05:30
Jannat Patel 5c51e01c78 fix: linter 2024-05-06 16:24:57 +05:30
Jannat Patel 650f81c22b fix: misc issues 2024-05-06 16:20:47 +05:30
Jannat Patel 3478f278ff fix: progress 2024-05-06 14:24:19 +05:30
Jannat Patel d53123cf07 Merge pull request #792 from dj12djdjs/fix-existing-role
fix: check if role exists before renaming
2024-05-03 16:01:19 +05:30
Jannat Patel cf5a088f5e fix: disable self learning 2024-05-03 11:38:41 +05:30
Jannat Patel 7d937eb024 Merge pull request #794 from arunmathaisk/develop
feat: CertifiedParticipants.vue page is now searchable
2024-05-03 10:56:18 +05:30
Arun Mathai SK 4edaad53a1 chore : change to Lucide icon, lint issues 2024-05-02 15:07:08 +00:00
Jannat Patel 8a2991c4fb fix: multiple badges 2024-05-02 19:43:32 +05:30
Arun Mathai SK 0c9fdc6534 feat: CertifiedParticipants.vue page is now searchable 2024-05-02 13:00:46 +00:00
Devin Slauenwhite 889bc2b1c7 fix: check if role exists before renaming 2024-04-30 20:27:29 -04:00
Jannat Patel 7355be2a8b feat: badges 2024-04-30 19:25:07 +05:30
Jannat Patel 0f64da69c0 Merge pull request #791 from pateljannat/issues-5
fix: misc fixes
2024-04-30 11:12:55 +05:30
Jannat Patel d9ad642a31 fix: misc fixes 2024-04-30 10:52:35 +05:30
Md Hussain Nagaria 180140c13f fix: attempts should be reloaded when quiz data changes (#790) 2024-04-29 20:03:11 +05:30
Jannat Patel e7d7cffbc5 fix: unavailability validations 2024-04-29 13:39:58 +05:30
Jannat Patel 29ccbddea8 Merge pull request #788 from pateljannat/issues-4
fix: seo link redirections and guest certificate
2024-04-26 16:31:53 +05:30
Jannat Patel 944020ca6e fix: seo link redirections and guest certificate 2024-04-26 16:20:09 +05:30
Bowrna 4bc07100f5 Update README.md
After the docker is up, we may need to log in to the app for first time. Those credentials were hidden inside the file and keeping it in README.md may be useful for new contributors like me. Also the docker-compose yaml, comes bundled with frappe bench therefore the following guidelines on installing the frappe bench may be irrelevant.
2024-04-26 14:26:52 +05:30
Jannat Patel 7dec8f019f Merge pull request #783 from pateljannat/lesson-issues-3
fix: misc lesson issues
2024-04-26 10:32:21 +05:30
Jannat Patel 1a2cb0fc3c fix: misc lesson issues 2024-04-25 16:41:56 +05:30
Jannat Patel 5dc3b62b94 Merge pull request #782 from pateljannat/quiz-shuffle
feat: quiz shuffle
2024-04-25 12:37:03 +05:30
Jannat Patel 4a4c8b4e7a fix: explanation issue 2024-04-25 12:27:00 +05:30
Jannat Patel c3390b9005 feat: quiz shuffle 2024-04-24 12:51:01 +05:30
Jannat Patel 804fc8e391 fix: quiz question rendering 2024-04-24 12:31:11 +05:30
Jannat Patel 5b5d779f25 fix: certified participants query 2024-04-24 11:27:50 +05:30
Jannat Patel 3fcf037db2 Merge pull request #781 from pateljannat/certified-users
feat: certified participants
2024-04-23 17:58:50 +05:30
Jannat Patel 324ede5523 fix: converted sql to qb 2024-04-23 17:52:21 +05:30
Jannat Patel e6e8718bb4 feat: certified participants 2024-04-23 17:29:16 +05:30
Jannat Patel 93e42fbd86 fix: zoom settings 2024-04-22 21:10:19 +05:30
Jannat Patel 8a6adae89c fix: lesson content rendering 2024-04-22 20:51:09 +05:30
Jannat Patel e3ad0baeb7 fix: batch details meta 2024-04-22 17:41:42 +05:30
Jannat Patel 953eb74235 fix: lesson duplicate video rendering 2024-04-22 17:18:53 +05:30
Jannat Patel b3c76e311c fix: unavailability validations 2024-04-22 16:56:07 +05:30
Jannat Patel f41eb30f3c Merge branch 'develop' of https://github.com/frappe/lms into develop 2024-04-22 16:50:18 +05:30
Jannat Patel dbe236f75e fix: edit course when no tags added 2024-04-22 16:50:10 +05:30
Jannat Patel 7d4a9eaf45 Merge pull request #779 from pateljannat/lesson-issue-1
fix: misc lesson fixes
2024-04-22 15:06:34 +05:30
Jannat Patel 4f7c3f14df fix: batch lists 2024-04-22 14:38:46 +05:30
Jannat Patel f15fdcc42e feat: code editor 2024-04-19 14:59:40 +05:30
Jannat Patel 44b36599c3 fix: profile routes 2024-04-18 20:27:15 +05:30
Jannat Patel 86713db75e fix: misc lesson fixes 2024-04-17 16:46:20 +05:30
Jannat Patel d2491b81c0 Merge pull request #778 from pateljannat/new-bill-job
feat: new tab and other misc fixes
2024-04-17 10:43:42 +05:30
Jannat Patel b98f6369ae feat: new tab and other misc fixes 2024-04-17 10:36:48 +05:30
Md Hussain Nagaria a2a210d82b Merge pull request #777 from frappe/fix-batch-date-range
fix: batch date range
2024-04-16 22:04:00 +05:30
Hussain Nagaria 5b120ad248 refactor: take format as optional argument 2024-04-16 22:02:56 +05:30
Hussain Nagaria 4ec57349f8 refactor: extract out component for date range 2024-04-16 21:39:19 +05:30
Hussain Nagaria f48f437075 fix: handle same start and end dates case for batch
* also minor refactor
2024-04-16 21:24:53 +05:30
Jannat Patel 255990b022 Merge pull request #774 from pateljannat/profile
feat: profile page
2024-04-16 17:37:37 +05:30
Jannat Patel ac44c59e50 fix: unavailability validaions 2024-04-16 17:28:45 +05:30
Jannat Patel 9252920a79 feat: google calendar integration 2024-04-16 16:56:18 +05:30
Jannat Patel 719e471678 feat: evaluator unavailability 2024-04-16 11:08:30 +05:30
Jannat Patel 39bc141133 Merge branch 'develop' of https://github.com/frappe/lms into profile 2024-04-15 13:55:35 +05:30
Jannat Patel 78698cfcbe Merge pull request #773 from pateljannat/youtube-issues
fix: lesson youtube video issue
2024-04-15 13:33:03 +05:30
Jannat Patel 64c02f14a9 fix: removed quiz_id from the lesson content 2024-04-15 13:29:50 +05:30
Jannat Patel 2f68fc0d6e fix: quiz rerender issue 2024-04-15 13:27:38 +05:30
Jannat Patel 16570469e6 fix: lesson youtube video issue 2024-04-15 12:35:27 +05:30
Jannat Patel f2c14d09d4 feat: profile settings for roles 2024-04-15 12:10:11 +05:30
Jannat Patel 7e81b9d45d Merge branch 'develop' of https://github.com/frappe/lms into profile 2024-04-15 10:06:58 +05:30
Md Hussain Nagaria a62b754d28 fix: empty the container before editorjs init (#771) 2024-04-12 13:52:43 +05:30
Jannat Patel 93859d6635 Merge pull request #769 from pateljannat/fixes-lesson
fix: check site conf before sending email
2024-04-10 21:23:15 +05:30
Jannat Patel 20fab8dbd3 fix: check site conf before sending email 2024-04-10 19:38:30 +05:30
Jannat Patel e56c8cc5f8 feat: profile page 2024-04-10 11:53:28 +05:30
Jannat Patel 13d0621881 feat: profile page 2024-04-10 11:53:00 +05:30
Md Hussain Nagaria 13536aac51 fix: primary page title (#767) 2024-04-09 15:33:10 +05:30
Jannat Patel ccb8721674 fix: evaluation modal 2024-04-06 11:19:49 +05:30
Jannat Patel fbe1423edd fix: meta for batches 2024-04-06 11:17:25 +05:30
Jannat Patel d2713d7824 Merge pull request #763 from pateljannat/slots-message
fix: no slots message
2024-04-06 11:03:26 +05:30
Jannat Patel f24a15b4f8 fix: no slots message 2024-04-06 10:55:50 +05:30
Jannat Patel bf74868bd4 Merge pull request #762 from pateljannat/assignments
feat: assignments in batches
2024-04-05 22:51:39 +05:30
Jannat Patel 6d75b8a3a2 feat: assignments in batches 2024-04-05 21:51:18 +05:30
Jannat Patel 055f917c61 Merge pull request #761 from pateljannat/rewrite-fixes
fix: mobile layout and certification link
2024-04-05 08:59:38 +05:30
Jannat Patel d892a28069 fix: mobile layout and certification link 2024-04-05 08:56:18 +05:30
Jannat Patel 838bc1fac2 Merge pull request #760 from pateljannat/fix-templates
fix: web templates
2024-04-04 15:41:14 +05:30
Jannat Patel a98d36513b fix: web templates 2024-04-04 15:01:22 +05:30
Jannat Patel 2cfa6771ac fix: lesson page issue 2024-04-04 11:24:44 +05:30
Jannat Patel 4960c47377 Merge pull request #758 from pateljannat/branding
fix: branding, course, lesson and certificate creation
2024-04-04 10:31:13 +05:30
Jannat Patel a46306720b fix: branding, course, lesson and certificate creation 2024-04-03 22:42:59 +05:30
Jannat Patel efcdba3a29 Merge pull request #713 from pateljannat/lms-frappe-ui
feat: LMS rewrite in Frappe UI
2024-04-03 12:22:03 +05:30
Jannat Patel e7263d0566 style: removed print statement 2024-04-03 12:14:51 +05:30
Jannat Patel 5de7f5e283 fix: progress 2024-04-03 12:11:34 +05:30
Jannat Patel c7bdf68bc6 bump: frappe-ui 2024-04-02 14:07:52 +05:30
Jannat Patel 998ff51c58 chore: removed print statements 2024-04-01 22:34:59 +05:30
Jannat Patel 4eb9d84d8b chore: fixed semgrep 2024-04-01 22:29:24 +05:30
Jannat Patel 5f000d8017 style: linters for vue files 2024-04-01 22:01:55 +05:30
Jannat Patel 0cf9ad5228 chore: resolved conflicts 2024-04-01 21:54:01 +05:30
Jannat Patel 71a526e7aa fix: dummy text to make content words greater than 250 2024-04-01 18:38:31 +05:30
Jannat Patel c5d9adb7fd fix: added paragraph for description and a link for seo 2024-04-01 17:47:34 +05:30
Jannat Patel 05fdf163a9 fix: certificate website link 2024-04-01 17:27:12 +05:30
Jannat Patel de0a983033 fix: replaced h2 with h1 for flash text 2024-03-29 22:44:17 +05:30
Jannat Patel 44ee9b644f fix: flash text for seo 2024-03-29 22:29:24 +05:30
Jannat Patel bd116c3e7b feat: meta for batches 2024-03-29 14:48:46 +05:30
Jannat Patel 02e8a97f85 chore: post install scripts 2024-03-28 17:20:39 +05:30
Jannat Patel 3525e4c90b feat: meta tags 2024-03-28 16:20:53 +05:30
Jannat Patel e6d3819092 feat: mobile responsive 2024-03-28 11:30:59 +05:30
Jannat Patel f15862cef4 fix: router 2024-03-19 12:51:11 +05:30
Jannat Patel 86748b301d feat: page titles 2024-03-19 12:04:58 +05:30
Jannat Patel cc07dd849c feat: job creation 2024-03-18 16:55:36 +05:30
Jannat Patel 63bcbb6506 feat: batch creation 2024-03-15 21:54:02 +05:30
Jannat Patel 83a1b03bb7 feat: quiz plugin in lesson 2024-03-11 09:38:27 +05:30
Jannat Patel 2905a6af1a Merge pull request #749 from pateljannat/batch-conditions
fix: batch registration button conditions
2024-03-07 10:33:39 +05:30
Jannat Patel 4cc27adb8b fix: batch registration button conditions 2024-03-07 10:21:18 +05:30
Jannat Patel 2126b4f657 fix: lesson editing 2024-03-07 10:06:32 +05:30
Jannat Patel 0ce7c74778 feat: lesson creation 2024-03-05 23:07:58 +05:30
Jannat Patel b9f6a23412 chore: build files 2024-03-04 22:12:21 +05:30
Jannat Patel 9ae96bd1fa feat: chapter creation 2024-03-04 22:10:51 +05:30
Jannat Patel e863abe37c fix: structure of ourline creation 2024-03-01 13:50:00 +05:30
Jannat Patel 80e9984db0 feat: course creation resources 2024-03-01 10:57:31 +05:30
Jannat Patel 8f504a8043 fix: course creation form validations 2024-02-28 23:42:17 +05:30
Jannat Patel 60a917e60c feat: course creation page structure 2024-02-27 20:41:02 +05:30
Jannat Patel a5d000f702 fix: quix show correct answers 2024-02-21 17:02:18 +05:30
Jannat Patel 5317aa8fb5 feat: job application 2024-02-21 12:08:05 +05:30
Jannat Patel 39aa1d443d fix: removed unnecessary start learning buttons 2024-02-19 17:07:22 +05:30
Jannat Patel 36c4e2f4dc feat: job application modal 2024-02-19 16:54:07 +05:30
Md Hussain Nagaria 8e7c1da7af Merge branch 'main' into main 2024-02-12 04:46:28 +03:00
ahmadRagheb 4f1f7c3fc0 Update edit.html
fix typo
2024-02-08 00:46:54 +02:00
Jannat Patel 084eeba2ed Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2024-02-07 12:25:37 +05:30
Jannat Patel 4b4086afb3 fix: active site capture 2024-02-07 11:14:12 +05:30
Jannat Patel 6401497422 feat: job details 2024-02-07 10:57:05 +05:30
Jannat Patel 684601f31b Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2024-02-07 10:55:50 +05:30
Jannat Patel d7d222842b feat: statistics graphs 2024-02-06 22:39:08 +05:30
Jannat Patel b0bb7d32ca chore: capture active sites 2024-02-06 10:12:49 +05:30
Jannat Patel ff1bd91223 feat: cohort description 2024-02-05 23:06:26 +05:30
Jannat Patel f59f6c617a fix: cleanup ui 2024-02-05 17:36:55 +05:30
Jannat Patel af48ccfb57 fix: cleanup ui 2024-02-05 17:35:46 +05:30
Jannat Patel 4b9d3bd996 fix: minor ui 2024-02-01 15:03:50 +05:30
Jannat Patel 3dad3580bb fix: batch self enrollment 2024-02-01 10:52:23 +05:30
Jannat Patel 53eb95612c fix: review button 2024-02-01 10:48:05 +05:30
Jannat Patel 8f687145be fix: pricing issue 2024-01-25 23:13:43 +05:30
Jannat Patel 9c405edd09 Merge branch 'main' of https://github.com/frappe/lms 2024-01-25 15:45:41 +05:30
Jannat Patel fe791dc478 fix: batch self enrollment 2024-01-25 15:45:27 +05:30
Jannat Patel 8f317d2f44 fix: ui issues 2024-01-25 15:24:52 +05:30
Jannat Patel f4e581f6cb fix: multicurrency on course cards 2024-01-23 17:03:15 +05:30
Jannat Patel 9671c4d63f feat: batch billing 2024-01-23 15:33:31 +05:30
Jannat Patel 42417621fa Merge pull request #712 from IslemMedjahdi/patch-1
Update docker-installation.md
2024-01-23 13:30:10 +05:30
Jannat Patel d3b3d85c84 fix: redirect to login if guest before batch enrollment 2024-01-22 11:19:32 +05:30
Md Hussain Nagaria b700013704 fix: only show start date for single day batches (#726) 2024-01-22 10:55:36 +05:30
Jannat Patel bac229c731 Merge pull request #725 from pateljannat/students-in-batch
feat: self enrolment in batches
2024-01-22 10:54:20 +05:30
Jannat Patel 28043e634b fix: removed unnecessary roles 2024-01-22 10:43:02 +05:30
Md Hussain Nagaria b672108155 fix: remove $ typos (#723) 2024-01-22 10:33:55 +05:30
Jannat Patel 5e569ab0e6 feat: self enrollment in batches 2024-01-19 23:56:48 +05:30
Jannat Patel b07940951c feat: billing page 2024-01-19 22:48:38 +05:30
Jannat Patel 1f18ef4362 feat: discussions in batches 2024-01-19 17:44:47 +05:30
Jannat Patel bf57a19e2c Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2024-01-18 14:44:20 +05:30
Jannat Patel 43a07e53a6 Merge branch 'main' of https://github.com/frappe/lms 2024-01-18 11:32:12 +05:30
Jannat Patel fbd83196fc fix: job page logos 2024-01-18 11:31:50 +05:30
Jannat Patel 465f4e1e96 docs: fixed readme logo dimensions 2024-01-17 14:48:48 +05:30
Jannat Patel 43d409ce64 Merge branch 'main' of https://github.com/frappe/lms 2024-01-17 14:47:11 +05:30
Jannat Patel a5fc52ec29 fix: logo on readme 2024-01-17 14:46:48 +05:30
Jannat Patel a9b06575d0 Merge pull request #718 from pateljannat/change-jobs-route
fix: changed jobs route to job-openings
2024-01-17 11:24:06 +05:30
Jannat Patel 3070cbed3c fix: changed jobs route to job-openings 2024-01-17 11:00:31 +05:30
Jannat Patel 0845a6e2a3 feat: statistics page numbers 2024-01-17 10:35:34 +05:30
Jannat Patel 041bae16e0 feat: discussions new topic 2024-01-16 20:49:07 +05:30
Jannat Patel 3313db844c feat: discussions edit and delete 2024-01-16 17:24:35 +05:30
Jannat Patel 3a5977a718 feat: discussions 2024-01-15 23:26:31 +05:30
Jannat Patel bcee74ce77 feat: batch announcements 2024-01-12 21:48:42 +05:30
Jannat Patel 1a6a119f35 feat: students and assessment tab in dashboard 2024-01-10 21:36:02 +05:30
Jannat Patel 09ae61492f feat: upcoming evals 2024-01-08 16:17:50 +05:30
Jannat Patel 3a33f047f5 feat: batch details 2024-01-05 18:22:03 +05:30
Jannat Patel 10cdd712d2 feat: batch card price and seat count 2024-01-02 12:39:28 +05:30
Jannat Patel 21959eef7b feat: quiz submission history table 2024-01-02 11:00:12 +05:30
Jannat Patel 41c3522285 feat: quiz submission 2024-01-01 23:26:53 +05:30
Medjahdi Islem d712881e16 Update docker-installation.md 2024-01-01 15:32:00 +01:00
Jannat Patel 991dc7f8c8 fix: batch card 2023-12-28 12:04:06 +05:30
Jannat Patel 7087fde686 feat: quiz base 2023-12-28 11:59:44 +05:30
Sai Phanindra a6ae5e0675 Update lms_batch.py 2023-12-27 11:31:17 +05:30
Sai Phanindra cbd5ae9969 Update lms_batch.py 2023-12-27 11:29:51 +05:30
Sai Phanindra 7389d080b6 Update lms_batch.py 2023-12-27 11:28:48 +05:30
Sai Phanindra 8defd664c5 Added batch start and end date validation 2023-12-24 00:06:56 +05:30
Jannat Patel 6b6c8da785 Merge pull request #706 from pateljannat/evaluation-request-validations
fix: evaluation dates validation
2023-12-22 22:54:23 +05:30
Jannat Patel e1d61c9eb9 feat: review submission 2023-12-21 17:25:08 +05:30
Jannat Patel afcb15148f chore: merged conflicts 2023-12-21 14:59:30 +05:30
Jannat Patel f40fbaed3e fix: check country from ip for multicurrency 2023-12-21 14:22:58 +05:30
Jannat Patel 5adb36deaf Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2023-12-21 14:05:59 +05:30
Jannat Patel 4973386dd0 Merge pull request #710 from pateljannat/usd-pricing
feat: usd pricing
2023-12-21 12:58:06 +05:30
Jannat Patel 13536b8bad feat: usd pricing 2023-12-21 12:28:11 +05:30
Jannat Patel caea7e334c Merge pull request #707 from pateljannat/batch-meta-image
fix: batch meta image saving
2023-12-21 09:56:50 +05:30
Jannat Patel 4065b1b8cc feat: review modal 2023-12-21 09:56:15 +05:30
Jannat Patel b248774774 Merge pull request #708 from pateljannat/confirmation-email
fix: copy of enrolment email
2023-12-20 12:16:03 +05:30
Jannat Patel 7a9d6325d5 fix: copy of enrollment email 2023-12-20 11:49:32 +05:30
Jannat Patel b0d0b41502 fix: batch meta image saving 2023-12-20 11:37:54 +05:30
Jannat Patel 30c89cb13c fix: evaluation dates validation 2023-12-20 11:00:09 +05:30
Jannat Patel eb3afbbad1 feat: rating component 2023-12-20 10:35:12 +05:30
Jannat Patel 9175737b9c Merge pull request #705 from pateljannat/email-address-assignment-issue
fix: assignment issue
2023-12-19 11:49:53 +05:30
Jannat Patel 7ae772205a Merge pull request #704 from pateljannat/evaluation-fields
fix: evaluation fields
2023-12-19 11:15:32 +05:30
Jannat Patel 00b0a20c83 fix: translations 2023-12-19 10:54:56 +05:30
Jannat Patel 6604866342 fix: assignment issue 2023-12-19 10:51:36 +05:30
Jannat Patel 881c3d943a fix: evaluation fields 2023-12-18 20:06:43 +05:30
Jannat Patel fbe219a888 feat: lesson pagination 2023-12-18 19:17:44 +05:30
Jannat Patel 5928b8e5f9 feat: lesson pagination 2023-12-18 19:17:17 +05:30
Jannat Patel 372425bed2 Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2023-12-18 11:31:35 +05:30
Jannat Patel d2922fd361 feat: lesson page 2023-12-15 23:39:15 +05:30
Jannat Patel d5118cc91f fix: razorpay payment in other currency 2023-12-14 16:08:07 +05:30
Jannat Patel ac74cbdf72 fix: razorpay payment in other currency 2023-12-14 15:53:07 +05:30
Jannat Patel 01f7fc3cff fix: razorpay payment in other currency 2023-12-14 15:12:23 +05:30
Jannat Patel 85c850e5bf fix: cohorts 2023-12-14 15:08:42 +05:30
Jannat Patel e7b6001e5f fix: logout issue 2023-12-14 14:32:50 +05:30
Jannat Patel 4053984ca2 fix: courses cahce 2023-12-13 11:57:25 +05:30
Jannat Patel a1e06bf316 fix: courses cahce 2023-12-13 11:57:18 +05:30
Jannat Patel 67dfffdd58 fix: calendar day view range 2023-12-13 10:41:15 +05:30
Jannat Patel c50f2147fd feat: course details page design 2023-12-13 10:33:34 +05:30
Jannat Patel d4671fb888 Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2023-12-12 14:58:45 +05:30
Jannat Patel ae4aadb8d3 Merge pull request #702 from pateljannat/timetable-mobile-view
feat: day view for timetable on mobile
2023-12-12 12:15:02 +05:30
Jannat Patel e5dc2bad6a feat: day view for timetable on mobile 2023-12-12 12:00:24 +05:30
Jannat Patel 77cda10419 feat: course details page 2023-12-12 10:19:52 +05:30
Jannat Patel 6de879cd2a fix: translations 2023-12-08 19:12:59 +05:30
Jannat Patel 0e2fabf139 Merge pull request #700 from pateljannat/fix-milestones
fix: future date access on timetables for moderators
2023-12-08 16:06:38 +05:30
Jannat Patel c45a372e83 fix: future date access on timetables for moderators 2023-12-08 15:27:33 +05:30
Jannat Patel 25f24b98c6 feat: fetch translations 2023-12-08 15:04:41 +05:30
Jannat Patel 98ecb4c27c Merge pull request #699 from pateljannat/all-day-events-style
fix: All day events style
2023-12-07 17:37:44 +05:30
Jannat Patel 9023094326 fix: timetable style 2023-12-07 16:00:11 +05:30
Jannat Patel 497de05db2 fix: all day events style 2023-12-07 15:53:10 +05:30
Jannat Patel 11079dae00 feat: translations 2023-12-07 11:38:12 +05:30
Jannat Patel d00da31f84 feat: course list 2023-12-05 22:39:00 +05:30
Jannat Patel 644fb698d8 chore: fixed config 2023-11-30 16:03:06 +05:30
Jannat Patel 92edb3a1bf Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2023-11-30 12:15:39 +05:30
Jannat Patel cb3224664e Merge pull request #695 from pateljannat/all-day-events
feat: all day events
2023-11-30 12:12:09 +05:30
Jannat Patel 9b532a5470 fix: editing evaluation end date from lms portal 2023-11-30 12:02:43 +05:30
Jannat Patel f1f9d9790b feat: all day events 2023-11-30 11:51:36 +05:30
Jannat Patel 96190910a7 Merge pull request #694 from pateljannat/evaluation-end-date
feat: evaluation end date
2023-11-29 22:58:53 +05:30
Jannat Patel 6484763d37 fix: removed additional roles 2023-11-29 22:32:52 +05:30
Jannat Patel 0e2feac81e fix: frappe-ui setup 2023-11-29 22:28:38 +05:30
Jannat Patel 6f1e7624ec feat:evaluation end date from lms portal 2023-11-29 18:01:44 +05:30
Jannat Patel eef5bd6062 feat: evaluation end date 2023-11-29 17:36:34 +05:30
Jannat Patel 63bcf15900 feat: course cards 2023-11-29 12:05:40 +05:30
Jannat Patel 25bcd10e93 Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2023-11-28 11:20:52 +05:30
Md Hussain Nagaria de60fbb25a fix(LMS Batch): add portal web link to form view (#692) 2023-11-27 22:19:07 +05:30
Jannat Patel fd9a638879 fix: quiz and timetable issues 2023-11-24 12:37:34 +05:30
Jannat Patel ddcb718a3a fix: quiz submission questions 2023-11-23 11:50:22 +05:30
Jannat Patel a17a7453e7 fix: ceil the percentage 2023-11-22 17:49:47 +05:30
Jannat Patel 479be0b8ee Merge pull request #681 from pateljannat/notify-mentions
feat: misc changes
2023-11-22 17:34:44 +05:30
Jannat Patel 6f40c357b3 fix: quiz submission page rendering 2023-11-22 17:27:42 +05:30
Jannat Patel 81db6c544d fix: mention email link 2023-11-22 13:04:04 +05:30
Jannat Patel be4e3aa963 Merge pull request #682 from pateljannat/course-creation-error
fix: course creation issue
2023-11-21 14:38:27 +05:30
Jannat Patel 6da0c07a3d fix: course creation issue 2023-11-21 14:13:11 +05:30
Jannat Patel b4ad10ca35 Merge pull request #680 from pateljannat/lesson-creation-issue
fix: encode chapter during lesson creation
2023-11-21 13:46:07 +05:30
Jannat Patel 2388b878dc fix: encode chapter during lesson creation 2023-11-21 13:35:47 +05:30
Jannat Patel 8cdaa7877a feat: discussions mention notifications 2023-11-21 13:32:12 +05:30
Jannat Patel d314287883 Merge pull request #679 from pateljannat/upcoming-batch
fix: upcoming batches based on start time
2023-11-17 11:00:07 +05:30
Jannat Patel b70dfc8e82 fix: upcoming batches based on start time 2023-11-17 10:45:29 +05:30
Jannat Patel 0a784766b4 Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui 2023-11-09 16:16:08 +05:30
Jannat Patel a5a7184f9a Merge pull request #676 from pateljannat/pyproject
build: added pyproject.toml
2023-11-09 15:54:12 +05:30
Jannat Patel 4e019d0a43 Merge pull request #677 from pateljannat/get-certificate-issue
fix: get certificate button visibility
2023-11-09 15:50:35 +05:30
Jannat Patel 8453b54360 fix: syntax of pyproject 2023-11-09 15:46:51 +05:30
Jannat Patel 9f9dfdb26d fix: get certificate button visibility 2023-11-09 15:27:46 +05:30
Jannat Patel 9fd4984247 fix: get certificate button visibility 2023-11-09 15:22:05 +05:30
Jannat Patel 9ebd64f47d build: added pyproject.toml 2023-11-09 15:18:06 +05:30
Jannat Patel 4316a37ed6 Merge pull request #675 from pateljannat/certificate-fix
fix: certificates in profile
2023-11-09 12:58:47 +05:30
Jannat Patel 2d745460e8 fix: certificates in profile 2023-11-09 12:29:23 +05:30
Jannat Patel b5258b6d9f Merge pull request #674 from pateljannat/assignment-fix
fix: assignment submission email
2023-11-07 12:05:21 +05:30
Jannat Patel 41b076c0db fix: user type filter on assignment submission 2023-11-07 11:42:31 +05:30
Jannat Patel 9d65e5e398 Merge pull request #673 from rtdany10/total-mark-issue
fix: mandatory total marks issue
2023-11-07 11:41:34 +05:30
Jannat Patel 7250bf7d65 fix: changed recipient in assignment submission email 2023-11-07 11:32:26 +05:30
Jannat Patel 4d7b247378 fix: assignment submission email 2023-11-07 11:31:58 +05:30
Dany Robert 0aaa58cd54 fix: mandatory total marks issue 2023-11-07 04:48:32 +00:00
Jannat Patel 014b85f12c Merge pull request #672 from pateljannat/user-category-default
fix: default user category
2023-11-06 19:14:19 +05:30
Jannat Patel 929f97cb72 test: skip invite email if in test 2023-11-06 19:08:30 +05:30
Jannat Patel de9cb935ee test: skip certificate email if in test 2023-11-06 18:58:12 +05:30
Jannat Patel 9aafc176e4 fix: default user category 2023-11-06 18:39:52 +05:30
Jannat Patel 0488ae8305 Merge pull request #670 from pateljannat/cert-fixes
fix: certificate border and email
2023-11-02 12:29:55 +05:30
Jannat Patel 60fd317d98 feat: certification email 2023-11-02 12:20:11 +05:30
Jannat Patel e54435d85d fix: certificate border 2023-11-01 18:27:17 +05:30
Jannat Patel 3a23b91c90 Merge pull request #669 from pateljannat/batch-tabs-customisation
feat: batch tabs settings
2023-10-30 18:42:56 +05:30
Jannat Patel 69591577bf feat: batch tabs settings 2023-10-30 18:30:58 +05:30
Jannat Patel e56afba6d3 Merge pull request #645 from tundebabzy/644
fix: 10th lesson access issue
2023-10-27 18:45:00 +05:30
Jannat Patel 98536ce4c7 Merge pull request #668 from frappe/pateljannat-security-md
chore: created security policy
2023-10-27 18:36:39 +05:30
Jannat Patel 05282178dd fix: removed functional programing code 2023-10-27 18:30:45 +05:30
Jannat Patel 1af547288c chore: fix linters 2023-10-27 17:53:35 +05:30
Jannat Patel b4af82acbc chore: fix linters 2023-10-27 17:21:28 +05:30
Jannat Patel 50fbe00d23 Merge pull request #667 from pateljannat/batch-ic
fix: misc batch issues
2023-10-27 17:18:20 +05:30
Jannat Patel b44428677e chore: created security policy 2023-10-27 17:17:10 +05:30
Tunde Akinyanmi d67faa1610 forgot to remove the LessonBookmark class 2023-10-27 12:05:37 +01:00
Tunde Akinyanmi 7b3f4c29d8 remove LessonBookmark abstraction. 2023-10-27 12:00:44 +01:00
Jannat Patel a49871c5b1 fix: misc batch issues 2023-10-27 16:04:03 +05:30
Jannat Patel e4005792af Merge pull request #665 from saadchaudharry/main
Fix:timetable validation
2023-10-27 12:04:36 +05:30
saadindictrans 8c0c09a21b Fix:timetable validation 2023-10-27 11:33:18 +05:30
Jannat Patel a9b05f4256 Merge pull request #662 from pateljannat/batch-source
feat: batch source
2023-10-26 20:35:57 +05:30
Jannat Patel cb6013a7a6 fix: source doctype name during install 2023-10-26 18:09:05 +05:30
Jannat Patel bb23b78a4f fix: made source mandatory in billing form 2023-10-26 18:00:11 +05:30
Jannat Patel 243277012f feat: batch source 2023-10-26 17:51:43 +05:30
Jannat Patel c9ed8a4b03 Merge pull request #661 from pateljannat/fix-eval-slot
fix: evaluation slots
2023-10-26 15:25:55 +05:30
Jannat Patel d413acaef3 fix: evaluation slots 2023-10-26 15:14:35 +05:30
Jannat Patel d6aad6cd74 Merge branch 'main' into 644 2023-10-26 14:45:56 +05:30
Jannat Patel ca45e43003 Merge pull request #658 from pateljannat/certification-fix
fix: certificate download template
2023-10-26 12:52:14 +05:30
Jannat Patel ad39530705 fix: certificate download template 2023-10-26 11:30:26 +05:30
Jannat Patel a6c2378b56 fix: certificate template pathc 2023-10-25 14:25:42 +05:30
Jannat Patel c073d2201d Merge pull request #657 from pateljannat/course-certificates
feat: certificate template
2023-10-25 14:15:54 +05:30
Jannat Patel 6d70de2eb1 feat: certificate template 2023-10-25 13:08:56 +05:30
Jannat Patel 48982e8f4a Merge pull request #655 from pateljannat/append-student-email
fix: Append student email to batch
2023-10-25 11:47:54 +05:30
Jannat Patel 397128f980 chore: merged conflicts 2023-10-25 11:36:07 +05:30
Jannat Patel 1d77fd3f94 Merge pull request #654 from pateljannat/timetable-milestones
feat: timetable milestones
2023-10-25 11:28:56 +05:30
Jannat Patel 60e78e8e74 feat: emails tab 2023-10-23 20:12:48 +05:30
Jannat Patel 4a9ccc6fde fix: cc in student email 2023-10-23 19:14:59 +05:30
Jannat Patel a707095fae fix: link student emails to batch 2023-10-23 19:11:36 +05:30
Jannat Patel d4f662f65e feat: timetable milestones 2023-10-23 16:35:35 +05:30
Jannat Patel 509b1365d9 Merge pull request #646 from pateljannat/quiz-refactor
feat: Quiz Refactor
2023-10-20 11:31:31 +05:30
Jannat Patel d0b236e381 fix: translations 2023-10-20 11:08:10 +05:30
Jannat Patel fe98265636 test: fix quiz tests 2023-10-20 10:56:47 +05:30
Jannat Patel 3f7d1b1e83 feat: add questions from LMS Portal 2023-10-19 22:15:30 +05:30
Jannat Patel 52cde329c1 Merge branch 'quiz-refactor' of https://github.com/pateljannat/lms into quiz-refactor 2023-10-18 23:21:57 +05:30
Jannat Patel 68b2dd6147 feat: quiz question from UI 2023-10-18 23:21:49 +05:30
Jannat Patel 5fa0d022dc Merge branch 'main' into quiz-refactor 2023-10-18 15:47:58 +05:30
Jannat Patel d996a5c53f Merge pull request #649 from NagariaHussain/feat-reply-to
feat: add reply_to in email students dialog
2023-10-18 12:35:46 +05:30
Jannat Patel b6dfc6ed4d Merge pull request #650 from NagariaHussain/feat-payment-info
feat: store batch/course in LMS Payment
2023-10-18 12:35:33 +05:30
Jannat Patel c7c2ba83f3 Merge branch 'main' into feat-payment-info 2023-10-18 12:12:19 +05:30
Hussain Nagaria 2bffabff05 style: fix linter 2023-10-18 12:11:23 +05:30
Hussain Nagaria 697e81df10 feat: add reply_to in email students 2023-10-18 12:11:23 +05:30
Jannat Patel f1b791845b Merge pull request #651 from pateljannat/editorjs-para
chore: bumped down paragraph plugin of editor js
2023-10-18 11:58:26 +05:30
Jannat Patel 6310845cdd chore: bunped down paragraph plugin of editor js 2023-10-18 11:47:15 +05:30
Hussain Nagaria 230cca63f3 feat: store batch/course in LMS payment 2023-10-17 23:18:43 +05:30
Hussain Nagaria af9f4d4b1e fix: remove unused import 2023-10-17 22:17:28 +05:30
Jannat Patel 0111ff9c99 feat: quiz marks and passing percentage 2023-10-17 20:06:04 +05:30
Jannat Patel 12bec14c92 feat: quiz validations and marks 2023-10-16 19:52:36 +05:30
Jannat Patel 174ea1ddd4 chore: resolved conflicts 2023-10-16 11:19:34 +05:30
Tunde Akinyanmi 038a7463e1 update get_neighbours to use LessonBookmark and
return the correct bookmark string
2023-10-13 18:39:11 +01:00
Tunde Akinyanmi a702909216 add LessonBookmark which is a data structure that
represents a bookmark.

While the underlying data structure is a tuple, it makes it easy to
abstract most of the logic we need and therefore allow the code to be
more readable.
2023-10-13 18:32:15 +01:00
Tunde Akinyanmi 8effd5614f remove cast operation from str to float.
It cause loss of the bookmark data
2023-10-13 18:14:11 +01:00
Jannat Patel 1046d28092 feat: quiz refactor 2023-10-11 12:25:46 +05:30
Jannat Patel 7678b89995 feat: setup frappe ui 2023-08-30 22:45:56 +05:30
478 changed files with 48188 additions and 11956 deletions
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -e
cd ~ || exit
echo "Setting Up Bench..."
pip install frappe-bench
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --frappe-branch "${BASE_BRANCH}"
cd ./frappe-bench || exit
echo "Get LMS..."
bench get-app --skip-assets lms "${GITHUB_WORKSPACE}"
echo "Generating POT file..."
bench generate-pot-file --app lms
cd ./apps/lms || exit
echo "Configuring git user..."
git config user.email "developers@erpnext.com"
git config user.name "frappe-pr-bot"
echo "Setting the correct git remote..."
# Here, the git remote is a local file path by default. Let's change it to the upstream repo.
git remote set-url upstream https://github.com/frappe/lms.git
echo "Creating a new branch..."
isodate=$(date -u +"%Y-%m-%d")
branch_name="pot_${BASE_BRANCH}_${isodate}"
git checkout -b "${branch_name}"
echo "Commiting changes..."
git add lms/locale/main.pot
git commit -m "chore: update POT file"
gh auth setup-git
git push -u upstream "${branch_name}"
echo "Creating a PR..."
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/lms
+34
View File
@@ -0,0 +1,34 @@
name: Regenerate POT file (translatable strings)
on:
schedule:
- cron: "00 16 * * 5"
workflow_dispatch:
jobs:
regenerate-pot-file:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch: ["develop"]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run script to update POT file
run: |
bash ${GITHUB_WORKSPACE}/.github/helper/update_pot_file.sh
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
BASE_BRANCH: ${{ matrix.branch }}
+1 -1
View File
@@ -30,4 +30,4 @@ jobs:
run: pip install semgrep
- name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules
run: semgrep ci --config ./frappe-semgrep-rules/rules
+27
View File
@@ -0,0 +1,27 @@
name: Create weekly release
on:
schedule:
# 13:00 UTC -> 7pm IST on every Wednesday
- cron: '30 4 * * 3'
workflow_dispatch:
jobs:
release:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: octokit/request-action@v2.x
with:
route: POST /repos/{owner}/{repo}/pulls
owner: frappe
repo: lms
title: |-
"chore: merge 'develop' into 'main'"
body: "Automated weekly release"
base: main
head: develop
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+32
View File
@@ -0,0 +1,32 @@
name: Generate Semantic Release
on:
workflow_dispatch:
push:
branches:
- main
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release
+39
View File
@@ -0,0 +1,39 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v2.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/generate-notes -f tag_name=$RELEASE_TAG \
| jq -r '.body' \
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
| sed -E 's/by @mergify //'
)
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}
+1
View File
@@ -99,6 +99,7 @@ jobs:
cd ~/frappe-bench/
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
bench --site lms.test set-password frappe@example.com admin
- name: cypress pre-requisites
run: |
+4 -1
View File
@@ -9,4 +9,7 @@ __pycache__/
*.py[cod]
*$py.class
node_modules
package-lock.json
package-lock.json
lms/public/frontend
lms/www/lms.html
frappe-ui
+3
View File
@@ -0,0 +1,3 @@
[submodule "frappe-ui"]
path = frappe-ui
url = https://github.com/frappe/frappe-ui
+1 -1
View File
@@ -32,7 +32,7 @@ repos:
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript]
types_or: [javascript, vue]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
+21
View File
@@ -0,0 +1,21 @@
{
"branches": ["develop"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular"
},
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" lms/__init__.py'
}
],
[
"@semantic-release/git", {
"assets": ["lms/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}"
}
],
"@semantic-release/github"
]
}
+8 -2
View File
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://www.frappelms.com/">
<img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px">
<img src="https://frappe.io/files/lms.png" alt="Frappe LMS" width="50px" height="50px">
</a>
<p align="center">Easy to use, open source, learning management system.</p>
</p>
@@ -75,7 +75,13 @@ cd apps/lms/docker
docker-compose up
```
Wait for some time until the setup script creates a site. After that, you can access `http://localhost:8000` in your browser and the app's login screen should show up.
Wait for some time until the setup script creates a site. After that, you can access `http://localhost:8000` in your browser and the app's login screen should appear.
You'll have to go through the setup wizard to set up the website the first time you access it. Log in using the following credentials to complete the setup wizard.
```
Username: Administrator
password: admin
```
### Frappe Bench
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process.
+8
View File
@@ -0,0 +1,8 @@
files:
- source: /lms/locale/main.pot
translation: /lms/locale/%two_letters_code%.po
pull_request_title: "chore: sync translations from crowdin"
pull_request_labels:
- translation
commit_message: "chore: %language% translations"
append_commit_message: false
+1 -1
View File
@@ -13,6 +13,6 @@ module.exports = defineConfig({
openMode: 0,
},
e2e: {
baseUrl: "http://dd1:8000",
baseUrl: "http://test_site_ui:8000",
},
});
+123 -97
View File
@@ -1,133 +1,159 @@
describe("Course Creation", () => {
it("creates a new course", () => {
cy.login();
cy.visit("/courses");
cy.wait(1000);
cy.visit("/lms/courses");
// Create a course
cy.get("a.btn").contains("Create a Course").click();
cy.wait(1000);
cy.url().should("include", "/courses/new-course/edit");
cy.get("#title").type("Test Course");
cy.get("#intro").type("Test Course Short Introduction");
cy.get("#description").type("Test Course Description");
cy.get("#video-link").type("-LPmw2Znl2c");
cy.get("#tags-input").type("Test");
cy.get("#published").check();
cy.get("a").contains("New").click();
cy.wait(1000);
cy.url().should("include", "/courses/new/edit");
cy.get("label").contains("Title").type("Test Course");
cy.get("label")
.contains("Short Introduction")
.type("Test Course Short Introduction to test the UI");
cy.get("div[contenteditable=true").invoke(
"text",
"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."
);
cy.fixture("profile.png", "base64").then((fileContent) => {
cy.get('input[type="file"]').attachFile({
fileContent,
fileName: "profile.png",
mimeType: "image/png",
encoding: "base64",
});
});
cy.get("label")
.contains("Preview Video")
.type("https://www.youtube.com/embed/-LPmw2Znl2c");
cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}");
cy.get("label")
.contains("Category")
.parent()
.within(() => {
cy.get("button").click();
});
cy.get("[id^=headlessui-combobox-option-")
.should("be.visible")
.first()
.click();
/* Instructor */
cy.get("label")
.contains("Instructors")
.parent()
.within(() => {
cy.get("input").click().type("frappe");
cy.get("input")
.invoke("attr", "aria-controls")
.as("instructor_list_id");
});
cy.get("@instructor_list_id").then((instructor_list_id) => {
cy.get(`[id^=${instructor_list_id}`)
.should("be.visible")
.within(() => {
cy.get("[id^=headlessui-combobox-option-").first().click();
});
});
cy.get("label").contains("Published").click();
cy.get("label").contains("Published On").type("2021-01-01");
cy.button("Save").click();
// Add Chapter
cy.wait(1000);
cy.link("Course Outline").click();
cy.button("Add Chapter").click();
cy.wait(1000);
cy.get(".edit-header .btn-add-chapter").click();
cy.wait(500);
cy.get("#chapter-title").type("Test Chapter");
cy.get("#chapter-description").type("Test Chapter Description");
cy.button("Save").click();
cy.get("[id^=headlessui-dialog-panel-")
.should("be.visible")
.within(() => {
cy.get("label").contains("Title").type("Test Chapter");
cy.button("Add Chapter").click();
});
// Add Lesson
cy.wait(1000);
cy.link("Add Lesson").click();
cy.button("Add Lesson").click();
cy.wait(1000);
cy.url().should("include", "/learn/1-1/edit");
cy.wait(1000);
cy.get("#lesson-title").type("Test Lesson");
// Content
cy.get(".collapse-section.collapsed:first").click();
cy.get("#lesson-content .ce-block")
.click()
.type(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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. {enter}"
);
cy.get("#lesson-content .ce-toolbar__plus").click();
cy.get('#lesson-content [data-item-name="youtube"]').click();
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
cy.button("Insert").click();
cy.wait(1000);
cy.get("label").contains("Title").type("Test Lesson");
cy.get("#content .ce-block").type(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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."
);
cy.button("Save").click();
// View Course
cy.wait(1000);
cy.visit("/courses");
cy.get(".course-card-title:first").contains("Test Course");
cy.get(".course-card:first").click();
cy.url().should("include", "/courses/test-course");
cy.get("#title").contains("Test Course");
cy.get(".preview-video").should(
cy.visit("/lms");
cy.wait(500);
cy.url().should("include", "/lms/courses");
cy.get(".grid a:first").within(() => {
cy.get("div").contains("Test Course");
cy.get("div").contains(
"Test Course Short Introduction to test the UI"
);
cy.get(".course-image")
.invoke("css", "background-image")
.should("include", "/files/profile");
});
cy.get(".grid a:first").click();
cy.url().should("include", "/lms/courses/test-course");
cy.get("div").contains("Test Course");
cy.get("div").contains("Test Course Short Introduction to test the UI");
cy.get("div").contains("Learning");
cy.get("div").contains("Frappe");
cy.get("div").contains("ERPNext");
cy.get("iframe").should(
"have.attr",
"src",
"https://www.youtube.com/embed/-LPmw2Znl2c"
);
cy.get("#intro").contains("Test Course Short Introduction");
// View Chapter
cy.get(".chapter-title-main:first").contains("Test Chapter");
cy.get(".chapter-description:first").contains(
"Test Chapter Description"
);
cy.get(".lesson-info:first").contains("Test Lesson");
cy.get(".lesson-info:first").click();
cy.get("div").contains("Test Chapter");
cy.get("[id^=headlessui-disclosure-panel-").within(() => {
cy.get("div").contains("Test Lesson").click();
});
cy.wait(3000);
// View Lesson
cy.wait(1000);
cy.url().should("include", "learn/1.1");
cy.get("#title").contains("Test Lesson");
cy.get(".lesson-video iframe").should(
"have.attr",
"src",
"https://www.youtube.com/embed/GoDtyItReto"
);
cy.get(".lesson-content-card").contains(
cy.url().should("include", "/learn/1-1");
cy.get("div").contains("Test Lesson");
cy.get("div").contains(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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."
);
// Add Discussion
cy.get(".reply").click();
cy.button("New Question").click();
cy.wait(500);
cy.get(".discussion-modal").should("be.visible");
// Enter title
cy.get(".modal .topic-title")
.type("Discussion from tests")
.should("have.value", "Discussion from tests");
// Enter comment
cy.get(".modal .discussions-comment").type(
"This is a discussion from the cypress ui tests."
);
// Submit
cy.get(".modal .submit-discussion").click();
cy.wait(2000);
// Check if discussion is added to page and content is visible
cy.get(".sidebar-parent:first .discussion-topic-title").should(
"have.text",
"Discussion from tests"
);
cy.get(".sidebar-parent:first .discussion-topic-title").click();
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(
".discussion-on-page:visible .reply-card .reply-text .ql-editor p"
).should(
"have.text",
"This is a discussion from the cypress ui tests."
);
cy.get(".discussion-form:visible .discussions-comment").type(
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
);
cy.get(".discussion-form:visible .submit-discussion").click();
cy.wait(3000);
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(".discussion-on-page:visible")
.children(".reply-card")
.eq(1)
.find(".reply-text")
.should(
"have.text",
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
cy.get("[id^=headlessui-dialog-panel-").within(() => {
cy.get("label").contains("Title").type("Test Discussion");
cy.get("div[contenteditable=true]").invoke(
"text",
"This is a test discussion. This will check if the UI is working properly."
);
cy.button("Post").click();
});
// View Discussion
cy.wait(500);
cy.get("div").contains("Test Discussion").click();
cy.get("div[contenteditable=true").invoke(
"text",
"This is a test comment. This will check if the UI is working properly."
);
cy.get("div").contains(
"This is a test comment. This will check if the UI is working properly."
);
});
});
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+12
View File
@@ -24,6 +24,8 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import "cypress-file-upload";
Cypress.Commands.add("login", (email, password) => {
if (!email) {
email = Cypress.config("testUser") || "Administrator";
@@ -53,3 +55,13 @@ Cypress.Commands.add("iconButton", (text) => {
Cypress.Commands.add("dialog", (selector) => {
return cy.get(`[role=dialog] ${selector}`);
});
Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => {
cy.wrap(subject).then(($element) => {
const element = $element[0];
element.focus();
element.textContent = text;
const event = new Event("paste", { bubbles: true });
element.dispatchEvent(event);
});
});
+2
View File
@@ -4,6 +4,8 @@
$ git clone https://github.com/frappe/lms.git
$ cd lms
$ cd docker
```
**Step 2:** Run docker-compose
-1
View File
@@ -35,7 +35,6 @@ bench new-site lms.localhost \
bench --site lms.localhost install-app lms
bench --site lms.localhost set-config developer_mode 1
bench --site lms.localhost clear-cache
bench --site lms.localhost set-config mute_emails 1
bench use lms.localhost
bench start
Submodule
+1
Submodule frappe-ui added at 8cd9b06a5e
+5
View File
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
+4
View File
@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}
+42
View File
@@ -0,0 +1,42 @@
# Frappe UI Starter
This template should help get you started developing custom frontend for Frappe
apps with Vue 3 and the Frappe UI package.
This boilerplate sets up Vue 3, Vue Router, TailwindCSS, and Frappe UI out of
the box.
## Usage
This template is meant to be cloned inside an existing Frappe App. Assuming your
apps name is `todo`. Clone this template in the root folder of your app using `degit`.
```
cd apps/todo
npx degit netchampfaris/frappe-ui-starter frontend
cd frontend
yarn
yarn dev
```
In a development environment, you need to put the below key-value pair in your `site_config.json` file:
```
"ignore_csrf": 1
```
This will prevent `CSRFToken` errors while using the vite dev server. In production environment, the `csrf_token` is attached to the `window` object in `index.html` for you.
The Vite dev server will start on the port `8080`. This can be changed from `vite.config.js`.
The development server is configured to proxy your frappe app (usually running on port `8000`). If you have a site named `todo.test`, open `http://todo.test:8080` in your browser. If you see a button named "Click to send 'ping' request", congratulations!
If you notice the browser URL is `/frontend`, this is the base URL where your frontend app will run in production.
To change this, open `src/router.js` and change the base URL passed to `createWebHistory`.
## Resources
- [Vue 3](https://v3.vuejs.org/guide/introduction.html)
- [Vue Router](https://next.router.vuejs.org/guide/)
- [Frappe UI](https://github.com/frappe/frappe-ui)
- [TailwindCSS](https://tailwindcss.com/docs/utility-first)
- [Vite](https://vitejs.dev/guide/)
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Frappe Learning</title>
<meta name="title" content="{{ meta.title }}" />
<meta name="image" content="{{ meta.image }}" />
<meta name="description" content="{{ meta.description }}" />
<meta name="keywords" content="{{ meta.keywords }}" />
<meta property="og:title" content="{{ meta.title }}" />
<meta property="og:image" content="{{ meta.image }}" />
<meta property="og:description" content="{{ meta.description }}" />
<meta name="twitter:title" content="{{ meta.title }}" />
<meta name="twitter:image" content="{{ meta.image }}" />
<meta name="twitter:description" content="{{ meta.description }}" />
</head>
<body>
<div id="app">
<div id="seo-content">
<h1>{{ meta.title }}</h1>
<p>
{{ meta.description }}
</p>
<p>
The content here is just for seo purposes. The actual content will be loaded in a few seconds.
</p>
<p>
Seo checks if a page has more than 300 words. So, here are some more words to make it more than 300 words.
Page descriptions are the HTML meta tags that provide a brief summary of a web page.
Search engines use meta descriptions to help identify the page's topic - they don't use them to rank the page, but they do use them to determine whether or not to display the page in search results.
Meta descriptions are important because they're often the first thing people see when they're deciding which search result to click on.
They're also important because they can help improve your click-through rate (CTR) from search results.
A good meta description can entice people to click on your page instead of someone else's.
</p>
<a href="{{ meta.link }}">Know More</a>
</div>
</div>
<div id="modals"></div>
<div id="popovers"></div>
<script>
window.csrf_token = '{{ csrf_token }}'
document.getElementById('seo-content').style.display = 'none';
</script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
{
"name": "frappe-ui-frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"serve": "vite preview",
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry",
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html"
},
"dependencies": {
"@editorjs/checklist": "^1.6.0",
"@editorjs/code": "^2.9.0",
"@editorjs/editorjs": "^2.29.0",
"@editorjs/embed": "^2.7.0",
"@editorjs/header": "^2.8.1",
"@editorjs/inline-code": "^1.5.0",
"@editorjs/nested-list": "^1.4.2",
"@editorjs/paragraph": "^2.11.3",
"@editorjs/simple-image": "^1.6.0",
"ace-builds": "^1.36.2",
"chart.js": "^4.4.1",
"codemirror-editor-vue3": "^2.8.0",
"dayjs": "^1.11.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.69",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",
"pinia": "^2.0.33",
"socket.io-client": "^4.7.2",
"tailwindcss": "^3.3.3",
"vue": "^3.4.23",
"vue-chartjs": "^5.3.0",
"vue-draggable-next": "^2.2.1",
"vue-router": "^4.0.12",
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.3",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"vite": "^5.0.11"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

+38
View File
@@ -0,0 +1,38 @@
<template>
<Layout>
<router-view />
</Layout>
<Dialogs />
<Toasts />
</template>
<script setup>
import { Toasts } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs'
import { computed, onMounted, onUnmounted } from 'vue'
import { useScreenSize } from './utils/composables'
import DesktopLayout from './components/DesktopLayout.vue'
import MobileLayout from './components/MobileLayout.vue'
import { stopSession } from '@/telemetry'
import { init as initTelemetry } from '@/telemetry'
import { usersStore } from '@/stores/user'
const screenSize = useScreenSize()
let { userResource } = usersStore()
const Layout = computed(() => {
if (screenSize.width < 640) {
return MobileLayout
} else {
return DesktopLayout
}
})
onMounted(async () => {
if (!userResource.data) return
await initTelemetry()
})
onUnmounted(() => {
stopSession()
})
</script>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+152
View File
@@ -0,0 +1,152 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("Inter-Thin.woff2?v=3.12") format("woff2"),
url("Inter-Thin.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"),
url("Inter-ThinItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLight.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("Inter-Light.woff2?v=3.12") format("woff2"),
url("Inter-Light.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"),
url("Inter-LightItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("Inter-Regular.woff2?v=3.12") format("woff2"),
url("Inter-Regular.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("Inter-Italic.woff2?v=3.12") format("woff2"),
url("Inter-Italic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("Inter-Medium.woff2?v=3.12") format("woff2"),
url("Inter-Medium.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"),
url("Inter-MediumItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"),
url("Inter-SemiBold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("Inter-Bold.woff2?v=3.12") format("woff2"),
url("Inter-Bold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-BoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("Inter-Black.woff2?v=3.12") format("woff2"),
url("Inter-Black.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"),
url("Inter-BlackItalic.woff?v=3.12") format("woff");
}
+62
View File
@@ -0,0 +1,62 @@
<template>
<div v-if="communications.data?.length">
<div v-for="comm in communications.data">
<div class="mb-8">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<Avatar :label="comm.sender_full_name" size="lg" />
<div class="ml-2">
{{ comm.sender_full_name }}
</div>
</div>
<div class="text-sm">
{{ timeAgo(comm.communication_date) }}
</div>
</div>
<div
class="prose prose-sm bg-gray-50 !min-w-full px-4 py-2 rounded-md"
v-html="comm.content"
></div>
</div>
</div>
</div>
<div v-else class="text-sm italic text-gray-600">
{{ __('No announcements') }}
</div>
</template>
<script setup>
import { createListResource, Avatar } from 'frappe-ui'
import { timeAgo } from '@/utils'
const props = defineProps({
batch: {
type: String,
required: true,
},
})
const communications = createListResource({
doctype: 'Communication',
fields: [
'subject',
'content',
'recipients',
'cc',
'communication_date',
'sender',
'sender_full_name',
],
filters: {
reference_doctype: 'LMS Batch',
reference_name: props.batch,
},
orderBy: 'communication_date desc',
auto: true,
cache: ['batch', props.batch],
})
</script>
<style>
.prose-sm p {
margin: 0 0 0.5rem;
}
</style>
+218
View File
@@ -0,0 +1,218 @@
<template>
<div
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
:class="isSidebarCollapsed ? 'w-14' : 'w-56'"
>
<div
class="flex flex-col overflow-hidden"
:class="isSidebarCollapsed ? 'items-center' : ''"
>
<UserDropdown :isCollapsed="isSidebarCollapsed" />
<div class="flex flex-col" v-if="sidebarSettings.data">
<SidebarLink
v-for="link in sidebarLinks"
:link="link"
:isCollapsed="isSidebarCollapsed"
class="mx-2 my-0.5"
/>
</div>
<div
v-if="sidebarSettings.data?.web_pages?.length || isModerator"
class="mt-4"
>
<div
class="flex items-center justify-between pr-2 cursor-pointer"
:class="isSidebarCollapsed ? 'pl-3' : 'pl-4'"
@click="showWebPages = !showWebPages"
>
<div
v-if="!isSidebarCollapsed"
class="flex items-center text-sm text-gray-600 my-1"
>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<ChevronRight
class="h-4 w-4 stroke-1.5 text-gray-900 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': showWebPages }"
/>
</span>
<span class="ml-2">
{{ __('More') }}
</span>
</div>
<Button v-if="isModerator" variant="ghost" @click="openPageModal()">
<template #icon>
<Plus class="h-4 w-4 text-gray-700 stroke-1.5" />
</template>
</Button>
</div>
<div
v-if="sidebarSettings.data?.web_pages?.length"
class="flex flex-col transition-all duration-300 ease-in-out"
:class="showWebPages ? 'block' : 'hidden'"
>
<SidebarLink
v-for="link in sidebarSettings.data.web_pages"
:link="link"
:isCollapsed="isSidebarCollapsed"
class="mx-2 my-0.5"
:showControls="isModerator ? true : false"
@openModal="openPageModal"
@deletePage="deletePage"
/>
</div>
</div>
</div>
<SidebarLink
:link="{
label: isSidebarCollapsed ? 'Expand' : 'Collapse',
}"
:isCollapsed="isSidebarCollapsed"
@click="isSidebarCollapsed = !isSidebarCollapsed"
class="m-2"
>
<template #icon>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<CollapseSidebar
class="h-4.5 w-4.5 text-gray-700 duration-300 ease-in-out"
:class="{ '[transform:rotateY(180deg)]': isSidebarCollapsed }"
/>
</span>
</template>
</SidebarLink>
</div>
<PageModal
v-model="showPageModal"
v-model:reloadSidebar="sidebarSettings"
:page="pageToEdit"
/>
</template>
<script setup>
import UserDropdown from '@/components/UserDropdown.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core'
import { ref, onMounted, inject, watch } from 'vue'
import { getSidebarLinks } from '../utils'
import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session'
import { ChevronRight, Plus } from 'lucide-vue-next'
import { createResource, Button } from 'frappe-ui'
import PageModal from '@/components/Modals/PageModal.vue'
const { user, sidebarSettings } = sessionStore()
const { userResource } = usersStore()
const socket = inject('$socket')
const unreadCount = ref(0)
const sidebarLinks = ref(getSidebarLinks())
const showPageModal = ref(false)
const isModerator = ref(false)
const isInstructor = ref(false)
const pageToEdit = ref(null)
const showWebPages = ref(false)
onMounted(() => {
socket.on('publish_lms_notifications', (data) => {
unreadNotifications.reload()
})
addNotifications()
sidebarSettings.reload(
{},
{
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label.toLowerCase().split(' ').join('_') !== key
)
}
})
},
}
)
})
const unreadNotifications = createResource({
cache: 'Unread Notifications Count',
url: 'frappe.client.get_count',
makeParams(values) {
return {
doctype: 'Notification Log',
filters: {
for_user: user,
read: 0,
},
}
},
onSuccess(data) {
unreadCount.value = data
sidebarLinks.value = sidebarLinks.value.map((link) => {
if (link.label === 'Notifications') {
link.count = data
}
return link
})
},
auto: user ? true : false,
})
const addNotifications = () => {
if (user) {
sidebarLinks.value.push({
label: 'Notifications',
icon: 'Bell',
to: 'Notifications',
activeFor: ['Notifications'],
count: unreadCount.value,
})
}
}
const addQuizzes = () => {
if (isInstructor.value || isModerator.value) {
sidebarLinks.value.push({
label: 'Quizzes',
icon: 'CircleHelp',
to: 'Quizzes',
activeFor: ['Quizzes', 'QuizForm'],
})
}
}
const openPageModal = (link) => {
showPageModal.value = true
pageToEdit.value = link
}
const deletePage = (link) => {
createResource({
url: 'lms.lms.api.delete_sidebar_item',
makeParams(values) {
return {
webpage: link.web_page,
}
},
}).submit(
{},
{
onSuccess() {
sidebarSettings.reload()
},
}
)
}
const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false)
}
watch(userResource, () => {
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addQuizzes()
}
})
let isSidebarCollapsed = ref(getSidebarFromStorage())
</script>
+67
View File
@@ -0,0 +1,67 @@
<template>
<Popover placement="right-start" class="flex w-full">
<template #target="{ togglePopover }">
<button
:class="[
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-gray-800 hover:bg-gray-100',
]"
@click.prevent="togglePopover()"
>
<div class="flex gap-2">
<LayoutGrid class="size-4 stroke-1.5" />
<span class="whitespace-nowrap">
{{ __('Apps') }}
</span>
</div>
<ChevronRight class="h-4 w-4 stroke-1.5" />
</button>
</template>
<template #body>
<div
class="grid grid-cols-3 justify-between mx-3 p-2 rounded-lg border border-gray-100 bg-white shadow-xl"
>
<div v-for="app in apps.data" key="name">
<a
:href="app.route"
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-3 hover:bg-gray-100"
>
<img class="size-8" :src="app.logo" />
<div class="text-sm" @click="app.onClick">
{{ app.title }}
</div>
</a>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import { Popover, createResource } from 'frappe-ui'
import { LayoutGrid, ChevronRight } from 'lucide-vue-next'
const apps = createResource({
url: 'frappe.apps.get_apps',
cache: 'apps',
auto: true,
transform: (data) => {
let _apps = [
{
name: 'frappe',
logo: '/assets/lms/images/desk.png',
title: __('Desk'),
route: '/app',
},
]
data.map((app) => {
if (app.name === 'lms') return
_apps.push({
name: app.name,
logo: app.logo,
title: __(app.title),
route: app.route,
})
})
return _apps
},
})
</script>
+196
View File
@@ -0,0 +1,196 @@
<template>
<div>
<div class="flex items-center justify-between">
<div class="text-lg font-semibold mb-4">
{{ __('Assessments') }}
</div>
<Button v-if="canSeeAddButton()" @click="showModal = true">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<div v-if="assessments.data?.length">
<ListView
:columns="getAssessmentColumns()"
:rows="assessments.data"
row-key="name"
:options="{
showTooltip: false,
getRowRoute: (row) => getRowRoute(row),
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
>
<ListHeaderItem :item="item" v-for="item in getAssessmentColumns()">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in assessments.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div>
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeAssessments(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
<div v-else class="text-sm italic text-gray-600">
{{ __('No Assessments') }}
</div>
</div>
<AssessmentModal
v-model="showModal"
v-model:assessments="assessments"
:batch="props.batch"
/>
</template>
<script setup>
import {
ListView,
ListRow,
ListRows,
ListHeader,
ListHeaderItem,
ListRowItem,
ListSelectBanner,
createResource,
Button,
} from 'frappe-ui'
import { inject, ref } from 'vue'
import AssessmentModal from '@/components/Modals/AssessmentModal.vue'
import { Plus, Trash2 } from 'lucide-vue-next'
const user = inject('$user')
const showModal = ref(false)
const props = defineProps({
batch: {
type: String,
required: true,
},
rows: {
type: Array,
},
columns: {
type: Array,
},
options: {
type: Object,
default: () => ({
selectable: true,
totalCount: 0,
rowCount: 0,
}),
},
})
const assessments = createResource({
url: 'lms.lms.utils.get_assessments',
params: {
batch: props.batch,
},
auto: true,
})
const deleteAssessments = createResource({
url: 'lms.lms.api.delete_documents',
makeParams(values) {
return {
doctype: 'LMS Assessment',
documents: values.assessments,
}
},
})
const removeAssessments = (selections, unselectAll) => {
deleteAssessments.submit(
{ assessments: Array.from(selections) },
{
onSuccess(data) {
assessments.reload()
unselectAll()
},
}
)
}
const getRowRoute = (row) => {
if (row.assessment_type == 'LMS Assignment') {
if (row.submission) {
return {
name: 'AssignmentSubmission',
params: {
assignmentName: row.assessment_name,
submissionName: row.submission.name,
},
}
} else {
return {
name: 'AssignmentSubmission',
params: {
assignmentName: row.assessment_name,
submissionName: 'new',
},
}
}
} else {
return {
name: 'QuizPage',
params: {
quizID: row.assessment_name,
},
}
}
}
const canSeeAddButton = () => {
return user.data?.is_moderator || user.data?.is_evaluator
}
const getAssessmentColumns = () => {
let columns = [
{
label: 'Assessment',
key: 'title',
},
{
label: 'Type',
key: 'assessment_type',
},
]
if (!user.data?.is_moderator) {
columns.push({
label: 'Status/Score',
key: 'status',
align: 'center',
})
}
return columns
}
</script>
+134
View File
@@ -0,0 +1,134 @@
<template>
<div>
<!-- <audio width="100%" controls controlsList="nodownload" class="mb-4">
<source :src="encodeURI(file)" type="audio/mp3" />
</audio> -->
<audio @ended="handleAudioEnd" controlsList="nodownload" class="mb-4">
<source :src="encodeURI(file)" type="audio/mp3" />
</audio>
<div class="flex items-center space-x-2 shadow rounded-lg p-1 w-1/2">
<Button variant="ghost" @click="togglePlay">
<template #icon>
<Play v-if="!isPlaying" class="w-4 h-4 text-gray-900" />
<Pause v-else class="w-4 h-4 text-gray-900" />
</template>
</Button>
<input
type="range"
min="0"
:max="duration"
step="0.1"
v-model="currentTime"
@input="changeCurrentTime"
class="duration-slider w-full h-1"
/>
<span class="text-xs text-gray-900 font-medium">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</span>
<Button variant="ghost" @click="toggleMute">
<template #icon>
<Volume2 v-if="!isMuted" class="w-4 h-4 text-gray-900" />
<VolumeX v-else class="w-4 h-4 text-gray-900" />
</template>
</Button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { Play, Pause, Volume2, VolumeX } from 'lucide-vue-next'
import { Button } from 'frappe-ui'
const isPlaying = ref(false)
const audio = ref(null)
let isMuted = ref(false)
let currentTime = ref(0)
let duration = ref(0)
const props = defineProps({
file: {
type: String,
required: true,
},
})
onMounted(() => {
setTimeout(() => {
audio.value = document.querySelector('audio')
audio.value.onloadedmetadata = () => {
duration.value = audio.value.duration
}
audio.value.ontimeupdate = () => {
currentTime.value = audio.value.currentTime
}
}, 0)
})
const togglePlay = () => {
if (audio.value.paused) {
audio.value.play()
isPlaying.value = true
} else {
audio.value.pause()
isPlaying.value = false
}
}
const toggleMute = () => {
audio.value.muted = !audio.value.muted
isMuted.value = audio.value.muted
}
const changeCurrentTime = () => {
audio.value.currentTime = currentTime.value
}
const handleAudioEnd = () => {
isPlaying.value = false
}
const formatTime = (time) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
}
watch(isPlaying, (newVal) => {
if (newVal) {
audio.value.play()
} else {
audio.value.pause()
}
})
</script>
<style>
.duration-slider {
flex: 1;
-webkit-appearance: none;
appearance: none;
background-color: theme('colors.gray.400');
cursor: pointer;
}
.duration-slider::-webkit-slider-thumb {
height: 10px;
width: 10px;
-webkit-appearance: none;
background-color: theme('colors.gray.900');
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type='range'] {
overflow: hidden;
width: 150px;
-webkit-appearance: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
cursor: pointer;
box-shadow: -150px 0 0 150px theme('colors.gray.900');
}
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<template>
<div
class="flex flex-col shadow hover:bg-gray-100 rounded-md p-4 h-full"
style="min-height: 150px"
>
<div class="text-lg leading-5 font-semibold mb-2">
{{ batch.title }}
</div>
<Badge
v-if="batch.seat_count && batch.seats_left > 0"
theme="green"
class="self-start mb-2"
>
{{ batch.seats_left }}
<span v-if="batch.seats_left > 1">{{ __('Seats Left') }}</span
><span v-else-if="batch.seats_left == 1">{{ __('Seat Left') }}</span>
</Badge>
<Badge
v-else-if="batch.seat_count && batch.seats_left <= 0"
theme="red"
class="self-start mb-2"
>
{{ __('Sold Out') }}
</Badge>
<div class="short-introduction text-sm text-gray-700">
{{ batch.description }}
</div>
<div v-if="batch.amount" class="font-semibold mb-4">
{{ batch.price }}
</div>
<div class="flex flex-col space-y-2 mt-auto">
<DateRange
:startDate="batch.start_date"
:endDate="batch.end_date"
class="text-sm text-gray-700"
/>
<div class="flex items-center text-sm text-gray-700">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
</span>
</div>
<div
v-if="batch.timezone"
class="flex items-center text-sm text-gray-700"
>
<Globe class="h-4 w-4 stroke-1.5 mr-2 text-gray-600" />
<span>
{{ batch.timezone }}
</span>
</div>
</div>
<div
v-if="batch.instructors?.length"
class="flex avatar-group overlap mt-4"
>
<div
class="h-6 mr-1"
:class="{ 'avatar-group overlap': batch.instructors.length > 1 }"
>
<UserAvatar
v-for="instructor in batch.instructors"
:user="instructor"
/>
</div>
<CourseInstructors :instructors="batch.instructors" />
</div>
</div>
</template>
<script setup>
import { Badge } from 'frappe-ui'
import { formatTime } from '../utils'
import { Clock, BookOpen, Globe } from 'lucide-vue-next'
import DateRange from '@/components/Common/DateRange.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
const props = defineProps({
batch: {
type: Object,
default: null,
},
})
</script>
<style>
.short-introduction {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
margin: 0.25rem 0 1rem;
line-height: 1.5;
}
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
}
</style>
+160
View File
@@ -0,0 +1,160 @@
<template>
<div>
<div class="flex items-center justify-between mb-4">
<div class="text-xl font-semibold">
{{ __('Courses') }}
</div>
<Button v-if="canSeeAddButton()" @click="openCourseModal()">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<div v-if="courses.data?.length">
<ListView
:columns="getCoursesColumns()"
:rows="courses.data"
row-key="batch_course"
:options="{
showTooltip: false,
getRowRoute: (row) => ({
name: 'CourseDetail',
params: { courseName: row.name },
}),
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
>
<ListHeaderItem :item="item" v-for="item in getCoursesColumns()">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in courses.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div>
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeCourses(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
<BatchCourseModal
v-model="showCourseModal"
:batch="batch"
v-model:courses="courses"
/>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
import BatchCourseModal from '@/components/Modals/BatchCourseModal.vue'
import {
createResource,
Button,
ListHeader,
ListHeaderItem,
ListSelectBanner,
ListRow,
ListRows,
ListView,
ListRowItem,
} from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils'
const showCourseModal = ref(false)
const user = inject('$user')
const props = defineProps({
batch: {
type: String,
required: true,
},
})
const courses = createResource({
url: 'lms.lms.utils.get_batch_courses',
params: {
batch: props.batch,
},
cache: ['batchCourses', props.batchName],
auto: true,
})
const openCourseModal = () => {
showCourseModal.value = true
}
const getCoursesColumns = () => {
return [
{
label: 'Title',
key: 'title',
width: 2,
},
{
label: 'Lessons',
key: 'lesson_count',
align: 'right',
},
{
label: 'Enrollments',
align: 'right',
key: 'enrollment_count',
},
]
}
const deleteCourses = createResource({
url: 'lms.lms.api.delete_documents',
makeParams(values) {
return {
doctype: 'Batch Course',
documents: values.courses,
}
},
})
const removeCourses = (selections, unselectAll) => {
deleteCourses.submit(
{
courses: Array.from(selections),
},
{
onSuccess(data) {
courses.reload()
showToast(__('Success'), __('Courses deleted successfully'), 'check')
unselectAll()
},
}
)
}
const canSeeAddButton = () => {
return user.data?.is_moderator || user.data?.is_evaluator
}
</script>
@@ -0,0 +1,26 @@
<template>
<div>
<UpcomingEvaluations
:batch="batch.data.name"
:endDate="batch.data.evaluation_end_date"
:courses="batch.data.courses"
:isStudent="isStudent"
/>
<Assessments :batch="batch.data.name" />
</div>
</template>
<script setup>
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
import Assessments from '@/components/Assessments.vue'
const props = defineProps({
batch: {
type: Object,
default: null,
},
isStudent: {
type: Boolean,
default: false,
},
})
</script>
+164
View File
@@ -0,0 +1,164 @@
<template>
<div v-if="batch.data" class="shadow rounded-md p-5 lg:w-72">
<Badge
v-if="batch.data.seat_count && seats_left > 0"
theme="green"
class="self-start mb-2 float-right"
>
{{ seats_left }} <span v-if="seats_left > 1">{{ __('Seats Left') }}</span
><span v-else-if="seats_left == 1">{{ __('Seat Left') }}</span>
</Badge>
<Badge
v-else-if="batch.data.seat_count && seats_left <= 0"
theme="red"
class="self-start mb-2 float-right"
>
{{ __('Sold Out') }}
</Badge>
<div v-if="batch.data.amount" class="text-lg font-semibold mb-3">
{{ formatNumberIntoCurrency(batch.data.amount, batch.data.currency) }}
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span> {{ batch.data.courses.length }} {{ __('Courses') }} </span>
</div>
<DateRange
:startDate="batch.data.start_date"
:endDate="batch.data.end_date"
class="mb-3"
/>
<div class="flex items-center mb-3">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div v-if="batch.data.timezone" class="flex items-center">
<Globe class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ batch.data.timezone }}
</span>
</div>
<router-link
v-if="isModerator || isStudent"
:to="{
name: 'Batch',
params: {
batchName: batch.data.name,
},
}"
>
<Button variant="solid" class="w-full mt-4">
<span>
{{ isModerator ? __('Manage Batch') : __('Visit Batch') }}
</span>
</Button>
</router-link>
<router-link
:to="{
name: 'Billing',
params: {
type: 'batch',
name: batch.data.name,
},
}"
v-else-if="batch.data.paid_batch && batch.data.seats_left"
>
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
<span>
{{ __('Register Now') }}
</span>
</Button>
</router-link>
<Button
variant="solid"
class="w-full mt-2"
v-else-if="batch.data.allow_self_enrollment && batch.data.seats_left"
@click="enrollInBatch()"
>
{{ __('Enroll Now') }}
</Button>
<router-link
v-if="isModerator"
:to="{
name: 'BatchForm',
params: {
batchName: batch.data.name,
},
}"
>
<Button class="w-full mt-2">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
</div>
</template>
<script setup>
import { inject, computed } from 'vue'
import { Badge, Button, createResource } from 'frappe-ui'
import { BookOpen, Clock, Globe } from 'lucide-vue-next'
import { formatNumberIntoCurrency, formatTime, showToast } from '@/utils'
import DateRange from '@/components/Common/DateRange.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const user = inject('$user')
const props = defineProps({
batch: {
type: Object,
default: null,
},
})
const enroll = createResource({
url: 'lms.lms.utils.enroll_in_batch',
makeParams(values) {
return {
batch: props.batch.data.name,
}
},
})
const enrollInBatch = () => {
if (!user.data) {
window.location.href = `/login?redirect-to=/batches/details/${props.batch.data.name}`
}
enroll.submit(
{},
{
onSuccess(data) {
showToast(
__('Success'),
__('You have been enrolled in this batch'),
'check'
)
router.push({
name: 'Batch',
params: {
batchName: props.batch.data.name,
},
})
},
}
)
}
const seats_left = computed(() => {
if (props.batch.data?.seat_count) {
return props.batch.data?.seat_count - props.batch.data?.students?.length
}
return null
})
const isStudent = computed(() => {
return props.batch.data?.students?.includes(user.data?.name)
})
const isModerator = computed(() => {
return user.data?.is_moderator
})
</script>
+163
View File
@@ -0,0 +1,163 @@
<template>
<Button class="float-right mb-3" @click="openStudentModal()">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add') }}
</Button>
<div class="text-lg font-semibold mb-4">
{{ __('Students') }}
</div>
<div v-if="students.data?.length">
<ListView
:columns="getStudentColumns()"
:rows="students.data"
row-key="name"
:options="{ showTooltip: false }"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
>
<ListHeaderItem :item="item" v-for="item in getStudentColumns()">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in students.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<template #prefix>
<div v-if="column.key == 'full_name'">
<Avatar
class="flex items-center"
:image="row['user_image']"
:label="item"
size="sm"
/>
</div>
</template>
<div>
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeStudents(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
<div v-else class="text-sm italic text-gray-600">
{{ __('There are no students in this batch.') }}
</div>
<StudentModal
:batch="props.batch"
v-model="showStudentModal"
v-model:reloadStudents="students"
/>
</template>
<script setup>
import {
createResource,
ListHeader,
ListHeaderItem,
ListSelectBanner,
ListRow,
ListRows,
ListView,
ListRowItem,
Avatar,
Button,
} from 'frappe-ui'
import { Trash2, Plus } from 'lucide-vue-next'
import { ref } from 'vue'
import StudentModal from '@/components/Modals/StudentModal.vue'
import { showToast } from '@/utils'
const showStudentModal = ref(false)
const props = defineProps({
batch: {
type: String,
default: null,
},
})
const students = createResource({
url: 'lms.lms.utils.get_batch_students',
cache: ['students', props.batch],
params: {
batch: props.batch,
},
auto: true,
})
const getStudentColumns = () => {
return [
{
label: 'Full Name',
key: 'full_name',
width: 2,
},
{
label: 'Courses Done',
key: 'courses_completed',
align: 'center',
},
{
label: 'Assessments Done',
key: 'assessments_completed',
align: 'center',
},
{
label: 'Last Active',
key: 'last_active',
},
]
}
const openStudentModal = () => {
showStudentModal.value = true
}
const deleteStudents = createResource({
url: 'lms.lms.api.delete_documents',
makeParams(values) {
return {
doctype: 'Batch Student',
documents: values.students,
}
},
})
const removeStudents = (selections, unselectAll) => {
deleteStudents.submit(
{
students: Array.from(selections),
},
{
onSuccess(data) {
students.reload()
showToast(__('Success'), __('Students deleted successfully'), 'check')
unselectAll()
},
}
)
}
</script>
+92
View File
@@ -0,0 +1,92 @@
<template>
<div class="flex flex-col justify-between min-h-0">
<div>
<div class="flex items-center justify-between">
<div class="font-semibold mb-1">
{{ __(label) }}
</div>
<Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
</div>
<div class="text-xs text-gray-600">
{{ __(description) }}
</div>
</div>
<div class="overflow-y-auto">
<SettingFields :fields="fields" :data="data.data" />
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" :loading="saveSettings.loading" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</div>
</template>
<script setup>
import { createResource, Button, Badge } from 'frappe-ui'
import SettingFields from '@/components/SettingFields.vue'
import { watch, ref } from 'vue'
const isDirty = ref(false)
const props = defineProps({
fields: {
type: Array,
required: true,
},
data: {
type: Object,
required: true,
},
label: {
type: String,
required: true,
},
description: {
type: String,
},
})
const saveSettings = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'Website Settings',
name: 'Website Settings',
fieldname: values.fields,
}
},
})
const update = () => {
let fieldsToSave = {}
let imageFields = ['favicon', 'banner_image', 'footer_logo']
props.fields.forEach((f) => {
if (imageFields.includes(f.name)) {
fieldsToSave[f.name] = f.value ? f.value.file_url : null
} else {
fieldsToSave[f.name] = f.value
}
})
saveSettings.submit(
{
fields: fieldsToSave,
},
{
onSuccess(data) {
isDirty.value = false
},
}
)
}
watch(props.data, (newData) => {
if (newData && !isDirty.value) {
isDirty.value = true
}
})
</script>
+151
View File
@@ -0,0 +1,151 @@
<template>
<div class="flex flex-col min-h-0">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold mb-1">
{{ label }}
</div>
<Button @click="() => showCategoryForm()">
<template #icon>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" />
</template>
</Button>
</div>
<div
v-if="showForm"
class="flex items-center justify-between my-4 space-x-2"
>
<FormControl
ref="categoryInput"
v-model="category"
:placeholder="__('Category Name')"
class="flex-1"
/>
<Button @click="addCategory()" variant="subtle">
{{ __('Add') }}
</Button>
</div>
<div class="overflow-y-scroll">
<div class="text-base divide-y">
<FormControl
:value="cat.category"
type="text"
v-for="cat in categories.data"
class="form-control"
@change.stop="(e) => update(cat.name, e.target.value)"
/>
</div>
</div>
</div>
</template>
<script setup>
import {
Button,
FormControl,
createListResource,
createResource,
debounce,
} from 'frappe-ui'
import { Plus, X } from 'lucide-vue-next'
import { ref } from 'vue'
const showForm = ref(false)
const category = ref(null)
const categoryInput = ref(null)
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
})
const categories = createListResource({
doctype: 'LMS Category',
fields: ['name', 'category'],
auto: true,
})
const newCategory = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'LMS Category',
category: category.value,
},
}
},
})
const addCategory = () => {
newCategory.submit(
{},
{
onSuccess(data) {
categories.reload()
category.value = null
},
}
)
}
const showCategoryForm = () => {
showForm.value = !showForm.value
setTimeout(() => {
categoryInput.value.$el.querySelector('input').focus()
}, 0)
}
const updateCategory = createResource({
url: 'frappe.client.rename_doc',
makeParams(values) {
return {
doctype: 'LMS Category',
old_name: values.name,
new_name: values.category,
}
},
})
const update = (name, value) => {
updateCategory.submit(
{
name: name,
category: value,
},
{
onSuccess() {
categories.reload()
},
}
)
}
</script>
<style>
.form-control input {
padding: 1.25rem 0;
border-color: transparent;
background: white;
}
.form-control input:focus {
outline: transparent;
background: white;
box-shadow: none;
border-color: transparent;
}
.form-control input:hover {
outline: transparent;
background: white;
box-shadow: none;
border-color: transparent;
}
</style>
@@ -0,0 +1,22 @@
<template>
<div class="flex items-center">
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ getFormattedDateRange(props.startDate, props.endDate) }}
</span>
</div>
</template>
<script setup>
import { Calendar } from 'lucide-vue-next'
import { getFormattedDateRange } from '@/utils'
const props = defineProps({
startDate: {
type: String,
},
endDate: {
type: String,
},
})
</script>
@@ -0,0 +1,286 @@
<template>
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ open: openPopover, togglePopover }">
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
<div class="w-full">
<button
class="flex w-full items-center justify-between focus:outline-none"
:class="inputClasses"
@click="() => togglePopover()"
>
<div class="flex items-center">
<slot name="prefix" />
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
v-if="selectedValue"
>
{{ displayValue(selectedValue) }}
</span>
<span class="text-base leading-5 text-gray-500" v-else>
{{ placeholder || '' }}
</span>
</div>
<ChevronDown class="h-4 w-4 stroke-1.5" />
</button>
</div>
</slot>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl">
<div class="relative px-1.5 pt-0.5">
<ComboboxInput
ref="search"
class="form-input w-full"
type="text"
@change="
(e) => {
query = e.target.value
}
"
:value="query"
autocomplete="off"
placeholder="Search"
/>
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="selectedValue = null"
>
<X class="h-4 w-4 stroke-1.5" />
</button>
</div>
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
>
<div
class="mt-1.5"
v-for="group in groups"
:key="group.key"
v-show="group.items.length > 0"
>
<div
v-if="group.group && !group.hideLabel"
class="px-2.5 py-1.5 text-sm font-medium text-gray-500"
>
{{ group.group }}
</div>
<ComboboxOption
as="template"
v-for="option in group.items"
:key="option.value"
:value="option"
v-slot="{ active, selected }"
>
<li
:class="[
'flex items-center rounded px-2.5 py-2 text-base',
{ 'bg-gray-100': active },
]"
>
<slot
name="item-prefix"
v-bind="{ active, selected, option }"
/>
<slot
name="item-label"
v-bind="{ active, selected, option }"
>
<div class="flex flex-col space-y-1">
<div>
{{ option.label }}
</div>
<div
v-if="option.label != option.description"
class="text-xs text-gray-700"
v-html="option.description"
></div>
</div>
</slot>
</li>
</ComboboxOption>
</div>
<li
v-if="groups.length == 0"
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-gray-600"
>
No results found
</li>
</ComboboxOptions>
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
<slot
name="footer"
v-bind="{ value: search?.el._value, close }"
></slot>
</div>
</div>
</div>
</template>
</Popover>
</Combobox>
</template>
<script setup>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import { Popover, Button } from 'frappe-ui'
import { ChevronDown, X } from 'lucide-vue-next'
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
options: {
type: Array,
default: () => [],
},
size: {
type: String,
default: 'md',
},
variant: {
type: String,
default: 'subtle',
},
placeholder: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
filterable: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['update:modelValue', 'update:query', 'change'])
const query = ref('')
const showOptions = ref(false)
const search = ref(null)
const attrs = useAttrs()
const slots = useSlots()
const valuePropPassed = computed(() => 'value' in attrs)
const selectedValue = computed({
get() {
return valuePropPassed.value ? attrs.value : props.modelValue
},
set(val) {
query.value = ''
if (val) {
showOptions.value = false
}
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val)
},
})
function close() {
showOptions.value = false
}
const groups = computed(() => {
if (!props.options || props.options.length == 0) return []
let groups = props.options[0]?.group
? props.options
: [{ group: '', items: props.options }]
return groups
.map((group, i) => {
return {
key: i,
group: group.group,
hideLabel: group.hideLabel || false,
items: props.filterable ? filterOptions(group.items) : group.items,
}
})
.filter((group) => group.items.length > 0)
})
function filterOptions(options) {
if (!query.value) {
return options
}
return options.filter((option) => {
let searchTexts = [option.label, option.value]
return searchTexts.some((text) =>
(text || '').toString().toLowerCase().includes(query.value.toLowerCase())
)
})
}
function displayValue(option) {
if (typeof option === 'string') {
let allOptions = groups.value.flatMap((group) => group.items)
let selectedOption = allOptions.find((o) => o.value === option)
return selectedOption?.label || option
}
return option?.label
}
watch(query, (q) => {
emit('update:query', q)
})
watch(showOptions, (val) => {
if (val) {
nextTick(() => {
search.value.el.focus()
})
}
})
const textColor = computed(() => {
return props.disabled ? 'text-gray-600' : 'text-gray-800'
})
const inputClasses = computed(() => {
let sizeClasses = {
sm: 'text-base rounded h-7',
md: 'text-base rounded h-8',
lg: 'text-lg rounded-md h-10',
xl: 'text-xl rounded-md h-10',
}[props.size]
let paddingClasses = {
sm: 'py-1.5 px-2',
md: 'py-1.5 px-2.5',
lg: 'py-1.5 px-3',
xl: 'py-1.5 px-3',
}[props.size]
let variant = props.disabled ? 'disabled' : props.variant
let variantClasses = {
subtle:
'border border-gray-100 bg-gray-100 placeholder-gray-500 hover:border-gray-200 hover:bg-gray-200 focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400',
outline:
'border border-gray-300 bg-white placeholder-gray-500 hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400',
disabled: [
'border bg-gray-50 placeholder-gray-400',
props.variant === 'outline' ? 'border-gray-300' : 'border-transparent',
],
}[variant]
return [
sizeClasses,
paddingClasses,
variantClasses,
textColor.value,
'transition-colors w-full',
]
})
defineExpose({ query })
</script>
@@ -0,0 +1,204 @@
<template>
<div
class="editor flex flex-col gap-1"
:style="{
height: height,
}"
>
<span class="text-xs" v-if="label">
{{ label }}
</span>
<div
ref="editor"
class="h-auto flex-1 overflow-hidden overscroll-none !rounded border border-outline-gray-2 bg-surface-gray-2 dark:bg-gray-900"
/>
<span
class="mt-1 text-xs text-gray-600"
v-show="description"
v-html="description"
></span>
<Button
v-if="showSaveButton"
@click="emit('save', aceEditor?.getValue())"
class="mt-3"
>
{{ __('Save') }}
</Button>
</div>
</template>
<script setup lang="ts">
import { useDark } from '@vueuse/core'
import ace from 'ace-builds'
import 'ace-builds/src-min-noconflict/ext-searchbox'
import 'ace-builds/src-min-noconflict/theme-chrome'
import 'ace-builds/src-min-noconflict/theme-twilight'
import { PropType, onMounted, ref, watch } from 'vue'
import { Button } from 'frappe-ui'
const isDark = useDark({
attribute: 'data-theme',
})
const props = defineProps({
modelValue: {
type: [Object, String, Array],
},
type: {
type: String as PropType<'JSON' | 'HTML' | 'Python' | 'JavaScript' | 'CSS'>,
default: 'JSON',
},
label: {
type: String,
default: '',
},
readonly: {
type: Boolean,
default: false,
},
height: {
type: String,
default: '250px',
},
showLineNumbers: {
type: Boolean,
default: false,
},
autofocus: {
type: Boolean,
default: true,
},
showSaveButton: {
type: Boolean,
default: false,
},
description: {
type: String,
default: '',
},
})
const emit = defineEmits(['save', 'update:modelValue'])
const editor = ref<HTMLElement | null>(null)
let aceEditor = null as ace.Ace.Editor | null
onMounted(() => {
setupEditor()
})
const setupEditor = () => {
aceEditor = ace.edit(editor.value as HTMLElement)
resetEditor(props.modelValue as string, true)
aceEditor.setReadOnly(props.readonly)
aceEditor.setOptions({
fontSize: '12px',
useWorker: false,
showGutter: props.showLineNumbers,
wrap: props.showLineNumbers,
})
if (props.type === 'CSS') {
import('ace-builds/src-noconflict/mode-css').then(() => {
aceEditor?.session.setMode('ace/mode/css')
})
} else if (props.type === 'JavaScript') {
import('ace-builds/src-noconflict/mode-javascript').then(() => {
aceEditor?.session.setMode('ace/mode/javascript')
})
} else if (props.type === 'Python') {
import('ace-builds/src-noconflict/mode-python').then(() => {
aceEditor?.session.setMode('ace/mode/python')
})
} else if (props.type === 'JSON') {
import('ace-builds/src-noconflict/mode-json').then(() => {
aceEditor?.session.setMode('ace/mode/json')
})
} else {
import('ace-builds/src-noconflict/mode-html').then(() => {
aceEditor?.session.setMode('ace/mode/html')
})
}
aceEditor.on('blur', () => {
try {
let value = aceEditor?.getValue() || ''
if (props.type === 'JSON') {
value = JSON.parse(value)
}
if (value === props.modelValue) return
if (!props.showSaveButton && !props.readonly) {
emit('update:modelValue', value)
}
} catch (e) {
// do nothing
}
})
}
const getModelValue = () => {
let value = props.modelValue || ''
try {
if (props.type === 'JSON' || typeof value === 'object') {
value = JSON.stringify(value, null, 2)
}
} catch (e) {
// do nothing
}
return value as string
}
function resetEditor(value: string, resetHistory = false) {
value = getModelValue()
aceEditor?.setValue(value)
aceEditor?.clearSelection()
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
props.autofocus && aceEditor?.focus()
if (resetHistory) {
aceEditor?.session.getUndoManager().reset()
}
}
watch(isDark, () => {
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
})
watch(
() => props.type,
() => {
setupEditor()
}
)
watch(
() => props.modelValue,
() => {
resetEditor(props.modelValue as string)
}
)
defineExpose({ resetEditor })
</script>
<style scoped>
.editor .ace_editor {
height: 100%;
width: 100%;
border-radius: 5px;
overscroll-behavior: none;
}
.editor :deep(.ace_scrollbar-h) {
display: none;
}
.editor :deep(.ace_search) {
@apply dark:bg-gray-800 dark:text-gray-200;
@apply dark:border-gray-800;
}
.editor :deep(.ace_searchbtn) {
@apply dark:bg-gray-800 dark:text-gray-200;
@apply dark:border-gray-800;
}
.editor :deep(.ace_button) {
@apply dark:bg-gray-800 dark:text-gray-200;
}
.editor :deep(.ace_search_field) {
@apply dark:bg-gray-900 dark:text-gray-200;
@apply dark:border-gray-800;
}
</style>
@@ -0,0 +1,114 @@
<template>
<div class="space-y-1.5">
<label class="block text-xs text-gray-600">
{{ label }}
</label>
<div class="w-full">
<Popover>
<template #target="{ togglePopover }">
<button
@click="openPopover(togglePopover)"
class="flex w-full items-center space-x-2 focus:outline-none bg-gray-100 rounded h-7 py-1.5 px-2 hover:bg-gray-200 focus:bg-white border border-gray-100 hover:border-gray-200 focus:border-gray-500"
>
<component
v-if="selectedIcon"
class="w-4 h-4 text-gray-700 stroke-1.5"
:is="icons[selectedIcon]"
/>
<component
v-else
class="w-4 h-4 text-gray-700 stroke-1.5"
:is="icons.Folder"
/>
<span v-if="selectedIcon">
{{ selectedIcon }}
</span>
<span v-else class="text-gray-600">
{{ __('Choose an icon') }}
</span>
</button>
</template>
<template #body-main="{ close, isOpen }" class="w-full">
<div class="p-3 max-h-56 overflow-auto w-full">
<FormControl
ref="search"
v-model="iconQuery"
:placeholder="__('Search for an icon')"
autocomplete="off"
/>
<div class="grid grid-cols-10 gap-4 mt-4">
<div v-for="(iconComponent, iconName) in filteredIcons">
<component
:is="iconComponent"
class="h-4 w-4 stroke-1.5 text-gray-700 cursor-pointer"
@click="setIcon(iconName, close)"
/>
</div>
</div>
</div>
</template>
</Popover>
</div>
</div>
</template>
<script setup>
import { FormControl, Popover } from 'frappe-ui'
import * as icons from 'lucide-vue-next'
import { ref, computed, onMounted, nextTick } from 'vue'
const iconQuery = ref('')
const selectedIcon = ref('')
const search = ref(null)
const emit = defineEmits(['update:modelValue', 'change'])
const iconArray = ref(
Object.keys(icons)
.sort(() => 0.5 - Math.random())
.slice(0, 100)
.reduce((result, key) => {
result[key] = icons[key]
return result
}, {})
)
const props = defineProps({
label: {
type: String,
default: 'Icon',
},
modelValue: {
type: String,
default: '',
},
})
onMounted(() => {
selectedIcon.value = props.modelValue
})
const setIcon = (icon, close) => {
emit('update:modelValue', icon)
selectedIcon.value = icon
iconQuery.value = ''
close()
}
const filteredIcons = computed(() => {
if (!iconQuery.value) {
return iconArray.value
}
return Object.keys(icons)
.filter((icon) =>
icon.toLowerCase().includes(iconQuery.value.toLowerCase())
)
.reduce((result, key) => {
result[key] = icons[key]
return result
}, {})
})
const openPopover = (togglePopover) => {
togglePopover()
}
</script>
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="space-y-1.5">
<label class="block" :class="labelClasses" v-if="attrs.label">
{{ attrs.label }}
</label>
<Autocomplete
ref="autocomplete"
:options="options.data"
v-model="value"
:size="attrs.size || 'sm'"
:variant="attrs.variant"
:placeholder="attrs.placeholder"
:filterable="false"
>
<template #target="{ open, togglePopover }">
<slot name="target" v-bind="{ open, togglePopover }" />
</template>
<template #prefix>
<slot name="prefix" />
</template>
<template #item-prefix="{ active, selected, option }">
<slot name="item-prefix" v-bind="{ active, selected, option }" />
</template>
<template #item-label="{ active, selected, option }">
<slot name="item-label" v-bind="{ active, selected, option }" />
</template>
<template v-if="attrs.onCreate" #footer="{ value, close }">
<div>
<Button
variant="ghost"
class="w-full !justify-start"
label="Create New"
@click="attrs.onCreate(value, close)"
>
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
</template>
</Autocomplete>
</div>
</template>
<script setup>
import Autocomplete from '@/components/Controls/Autocomplete.vue'
import { watchDebounced } from '@vueuse/core'
import { createResource, Button } from 'frappe-ui'
import { Plus } from 'lucide-vue-next'
import { useAttrs, computed, ref } from 'vue'
const props = defineProps({
doctype: {
type: String,
required: true,
},
filters: {
type: Object,
default: () => ({}),
},
modelValue: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'change'])
const attrs = useAttrs()
const valuePropPassed = computed(() => 'value' in attrs)
const value = computed({
get: () => (valuePropPassed.value ? attrs.value : props.modelValue),
set: (val) => {
return (
val?.value &&
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val?.value)
)
},
})
const autocomplete = ref(null)
const text = ref('')
watchDebounced(
() => autocomplete.value?.query,
(val) => {
val = val || ''
if (text.value === val) return
text.value = val
reload(val)
},
{ debounce: 300, immediate: true }
)
watchDebounced(
() => props.doctype,
() => reload(''),
{ debounce: 300, immediate: true }
)
const options = createResource({
url: 'frappe.desk.search.search_link',
cache: [props.doctype, text.value],
method: 'POST',
auto: true,
params: {
txt: text.value,
doctype: props.doctype,
filters: props.filters,
},
transform: (data) => {
return data.map((option) => {
return {
label: option.value,
value: option.value,
description: option.description,
}
})
},
})
function reload(val) {
options.update({
params: {
txt: val,
doctype: props.doctype,
filters: props.filters,
},
})
options.reload()
}
const labelClasses = computed(() => {
return [
{
sm: 'text-xs',
md: 'text-base',
}[attrs.size || 'sm'],
'text-gray-600',
]
})
</script>
@@ -0,0 +1,242 @@
<template>
<div>
<label class="block mb-1" :class="labelClasses" v-if="label">
{{ label }}
</label>
<div class="grid grid-cols-3 gap-1">
<Button
ref="emails"
v-for="value in values"
:key="value"
:label="value"
theme="gray"
variant="subtle"
class="rounded-md"
@keydown.delete.capture.stop="removeLastValue"
>
<template #suffix>
<X @click="removeValue(value)" class="h-4 w-4 stroke-1.5" />
</template>
</Button>
<div class="">
<Combobox v-model="selectedValue" nullable>
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ togglePopover }">
<ComboboxInput
ref="search"
class="search-input form-input w-full focus-visible:!ring-0"
type="text"
:value="query"
@change="
(e) => {
query = e.target.value
showOptions = true
}
"
autocomplete="off"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl">
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
>
<ComboboxOption
v-for="option in options"
:key="option.value"
:value="option"
v-slot="{ active }"
>
<li
:class="[
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
{ 'bg-gray-100': active },
]"
>
<div class="flex flex-col gap-1 p-1">
<div class="text-base font-medium">
{{ option.description }}
</div>
<div class="text-sm text-gray-600">
{{ option.value }}
</div>
</div>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</template>
</Popover>
</Combobox>
</div>
</div>
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
</div>
</template>
<script setup>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { X } from 'lucide-vue-next'
const props = defineProps({
label: {
type: String,
},
size: {
type: String,
default: 'sm',
},
doctype: {
type: String,
required: true,
},
filters: {
type: Object,
default: () => ({}),
},
validate: {
type: Function,
default: null,
},
errorMessage: {
type: Function,
default: (value) => `${value} is an Invalid value`,
},
})
const values = defineModel()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const query = ref('')
const text = ref('')
const showOptions = ref(false)
const selectedValue = computed({
get: () => query.value || '',
set: (val) => {
query.value = ''
if (val) {
showOptions.value = false
}
val?.value && addValue(val.value)
},
})
watchDebounced(
query,
(val) => {
val = val || ''
if (text.value === val) return
text.value = val
reload(val)
},
{ debounce: 300, immediate: true }
)
const filterOptions = createResource({
url: 'frappe.desk.search.search_link',
method: 'POST',
cache: [text.value, props.doctype],
auto: true,
params: {
txt: text.value,
doctype: props.doctype,
},
})
const options = computed(() => {
return filterOptions.data || []
})
function reload(val) {
filterOptions.update({
params: {
txt: val,
doctype: props.doctype,
},
})
filterOptions.reload()
}
const addValue = (value) => {
error.value = null
if (value) {
const splitValues = value.split(',')
splitValues.forEach((value) => {
value = value.trim()
if (value) {
// check if value is not already in the values array
if (!values.value?.includes(value)) {
// check if value is valid
if (value && props.validate && !props.validate(value)) {
error.value = props.errorMessage(value)
return
}
// add value to values array
if (!values.value) {
values.value = [value]
} else {
values.value.push(value)
}
value = value.replace(value, '')
}
}
})
!error.value && (value = '')
}
}
const removeValue = (value) => {
values.value = values.value.filter((v) => v !== value)
}
const removeLastValue = () => {
if (query.value) return
let emailRef = emails.value[emails.value.length - 1]?.$el
if (document.activeElement === emailRef) {
values.value.pop()
nextTick(() => {
if (values.value.length) {
emailRef = emails.value[emails.value.length - 1].$el
emailRef?.focus()
} else {
setFocus()
}
})
} else {
emailRef?.focus()
}
}
function setFocus() {
search.value.$el.focus()
}
defineExpose({ setFocus })
const labelClasses = computed(() => {
return [
{
sm: 'text-xs',
md: 'text-base',
}[props.size || 'sm'],
'text-gray-600',
]
})
</script>
@@ -0,0 +1,81 @@
<template>
<div class="space-y-1">
<label class="block text-xs text-gray-600" v-if="props.label">
{{ props.label }}
</label>
<div class="flex text-center">
<div
v-for="index in 5"
@mouseover="hoveredRating = index"
@mouseleave="hoveredRating = 0"
>
<Star
class="fill-gray-400 text-gray-50 stroke-1 mr-1 cursor-pointer"
:class="iconClasses(index)"
@click="markRating(index)"
/>
</div>
</div>
</div>
</template>
<script setup>
import { Star } from 'lucide-vue-next'
import { ref, watch } from 'vue'
const props = defineProps({
id: {
type: String,
default: '',
},
modelValue: {
type: Number,
default: 0,
},
label: {
type: String,
default: '',
},
size: {
type: String,
default: 'md',
},
})
const iconClasses = (index) => {
let classes = [
{
sm: 'size-4',
md: 'size-5',
lg: 'size-6',
xl: 'size-7',
}[props.size],
]
if (index <= hoveredRating.value && index > rating.value) {
classes.push('fill-yellow-200')
} else if (index <= rating.value) {
classes.push('fill-yellow-500')
}
return classes.join(' ')
}
const emit = defineEmits(['update:modelValue'])
const rating = ref(props.modelValue)
const hoveredRating = ref(0)
let emitChange = (value) => {
emit('update:modelValue', value)
}
function markRating(index) {
emitChange(index)
rating.value = index
}
watch(
() => props.modelValue,
(newVal) => {
rating.value = newVal
}
)
</script>
+186
View File
@@ -0,0 +1,186 @@
<template>
<div
v-if="course.title"
class="flex flex-col h-full rounded-md shadow-md text-base overflow-auto"
style="min-height: 350px"
>
<div
class="course-image"
:class="{ 'default-image': !course.image }"
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
>
<div
class="flex items-center flex-wrap space-y-1 space-x-1 relative top-4 px-2 w-fit"
>
<Badge v-if="course.featured" variant="subtle" theme="green" size="md">
{{ __('Featured') }}
</Badge>
<Badge
variant="outline"
theme="gray"
size="md"
v-for="tag in course.tags"
>
{{ tag }}
</Badge>
</div>
<div v-if="!course.image" class="image-placeholder">
{{ course.title[0] }}
</div>
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2">
<div v-if="course.lesson_count">
<Tooltip :text="__('Lessons')">
<span class="flex items-center">
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
{{ course.lesson_count }}
</span>
</Tooltip>
</div>
<div v-if="course.enrollment_count">
<Tooltip :text="__('Enrolled Students')">
<span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
{{ course.enrollment_count }}
</span>
</Tooltip>
</div>
<div v-if="course.avg_rating">
<Tooltip :text="__('Average Rating')">
<span class="flex items-center">
<Star class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
{{ course.avg_rating }}
</span>
</Tooltip>
</div>
<div v-if="course.status != 'Approved'">
<Badge
variant="solid"
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
size="sm"
>
{{ course.status }}
</Badge>
</div>
</div>
<div class="text-xl font-semibold leading-6">
{{ course.title }}
</div>
<div class="short-introduction text-gray-700 text-sm">
{{ course.short_introduction }}
</div>
<ProgressBar
v-if="user && course.membership"
:progress="course.membership.progress"
/>
<div v-if="user && course.membership" class="text-sm mb-4">
{{ Math.ceil(course.membership.progress) }}% completed
</div>
<div class="flex items-center justify-between mt-auto">
<div class="flex avatar-group overlap">
<div
class="h-6 mr-1"
:class="{ 'avatar-group overlap': course.instructors.length > 1 }"
>
<UserAvatar
v-for="instructor in course.instructors"
:user="instructor"
/>
</div>
<CourseInstructors :instructors="course.instructors" />
</div>
<div class="font-semibold">
{{ course.price }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { sessionStore } from '@/stores/session'
import { Badge, Tooltip } from 'frappe-ui'
import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue'
const { user } = sessionStore()
const props = defineProps({
course: {
type: Object,
default: null,
},
})
</script>
<style>
.course-image {
height: 168px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.course-card-pills {
background: #ffffff;
margin-left: 0;
margin-right: 0.5rem;
padding: 3.5px 8px;
font-size: 11px;
text-align: center;
letter-spacing: 0.011em;
text-transform: uppercase;
font-weight: 600;
width: fit-content;
}
.default-image {
display: flex;
flex-direction: column;
align-items: center;
background-color: theme('colors.green.100');
color: theme('colors.green.600');
}
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
.image-placeholder {
display: flex;
align-items: center;
flex: 1;
font-size: 5rem;
color: theme('colors.gray.700');
font-weight: 600;
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
}
.short-introduction {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
margin: 0.25rem 0 1.25rem;
line-height: 1.5;
}
</style>
@@ -0,0 +1,224 @@
<template>
<div class="shadow rounded-md min-w-80">
<iframe
v-if="course.data.video_link"
:src="video_link"
class="rounded-t-md min-h-56 w-full"
/>
<div class="p-5">
<div v-if="course.data.price" class="text-2xl font-semibold mb-3">
{{ course.data.price }}
</div>
<router-link
v-if="course.data.membership"
:to="{
name: 'Lesson',
params: {
courseName: course.name,
chapterNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[0]
: 1,
lessonNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[1]
: 1,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Continue Learning') }}
</span>
</Button>
</router-link>
<router-link
v-else-if="course.data.paid_course"
:to="{
name: 'Billing',
params: {
type: 'course',
name: course.data.name,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Buy this course') }}
</span>
</Button>
</router-link>
<div
v-else-if="course.data.disable_self_learning"
class="bg-blue-100 text-blue-900 text-sm rounded-md py-1 px-3"
>
{{ __('Contact the Administrator to enroll for this course.') }}
</div>
<Button
v-else
@click="enrollStudent()"
variant="solid"
class="w-full"
size="md"
>
<span>
{{ __('Start Learning') }}
</span>
</Button>
<Button
v-if="canGetCertificate"
@click="fetchCertificate()"
variant="subtle"
class="w-full mt-2"
size="md"
>
{{ __('Get Certificate') }}
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
name: 'CourseForm',
params: {
courseName: course.data.name,
},
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
<div class="mt-8 mb-4 font-medium">
{{ __('This course has:') }}
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-5 w-5 stroke-1.5 text-gray-600" />
<span class="ml-2">
{{ course.data.lesson_count }} {{ __('Lessons') }}
</span>
</div>
<div class="flex items-center mb-3">
<Users class="h-5 w-5 stroke-1.5 text-gray-600" />
<span class="ml-2">
{{ course.data.enrollment_count_formatted }}
{{ __('Enrolled Students') }}
</span>
</div>
<div class="flex items-center">
<Star class="h-5 w-5 stroke-1.5 fill-orange-500 text-gray-50" />
<span class="ml-2">
{{ course.data.avg_rating }} {{ __('Rating') }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import { computed, inject } from 'vue'
import { Button, createResource } from 'frappe-ui'
import { showToast } from '@/utils/'
import { capture } from '@/telemetry'
import { useRouter } from 'vue-router'
const router = useRouter()
const user = inject('$user')
const props = defineProps({
course: {
type: Object,
default: null,
},
})
const video_link = computed(() => {
if (props.course.data.video_link) {
return 'https://www.youtube.com/embed/' + props.course.data.video_link
}
return null
})
function enrollStudent() {
if (!user.data) {
showToast(
__('Please Login'),
__('You need to login first to enroll for this course'),
'circle-warn'
)
setTimeout(() => {
window.location.href = `/login?redirect-to=${window.location.pathname}`
}, 2000)
} else {
const enrollStudentResource = createResource({
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
})
enrollStudentResource
.submit({
course: props.course.data.name,
})
.then(() => {
capture('enrolled_in_course', {
course: props.course.data.name,
})
showToast(
__('Success'),
__('You have been enrolled in this course'),
'check'
)
setTimeout(() => {
router.push({
name: 'Lesson',
params: {
courseName: props.course.data.name,
chapterNumber: 1,
lessonNumber: 1,
},
})
}, 2000)
})
}
}
const is_instructor = () => {
let user_is_instructor = false
props.course.data.instructors.forEach((instructor) => {
if (!user_is_instructor && instructor.name == user.data?.name) {
user_is_instructor = true
}
})
return user_is_instructor
}
const canGetCertificate = computed(() => {
if (
props.course.data?.enable_certification &&
props.course.data?.membership?.progress == 100
) {
return true
}
return false
})
const certificate = createResource({
url: 'lms.lms.doctype.lms_certificate.lms_certificate.create_certificate',
makeParams(values) {
return {
course: values.course,
}
},
onSuccess(data) {
window.open(
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
data.name
}&format=${encodeURIComponent(data.template)}`,
'_blank'
)
},
})
const fetchCertificate = () => {
certificate.submit({
course: props.course.data?.name,
member: user.data?.name,
})
}
</script>
@@ -0,0 +1,50 @@
<template>
<span v-if="instructors.length == 1">
<router-link
:to="{
name: 'Profile',
params: { username: instructors[0].username },
}"
>
{{ instructors[0].full_name }}
</router-link>
</span>
<span v-if="instructors.length == 2">
<router-link
:to="{
name: 'Profile',
params: { username: instructors[0].username },
}"
>
{{ instructors[0].first_name }}
</router-link>
and
<router-link
:to="{
name: 'Profile',
params: { username: instructors[1].username },
}"
>
{{ instructors[1].first_name }}
</router-link>
</span>
<span v-if="instructors.length > 2">
<router-link
:to="{
name: 'Profile',
params: { username: instructors[0].username },
}"
>
{{ instructors[0].first_name }}
</router-link>
and {{ instructors.length - 1 }} others
</span>
</template>
<script setup>
const props = defineProps({
instructors: {
type: Array,
required: true,
},
})
</script>
+253
View File
@@ -0,0 +1,253 @@
<template>
<div class="text-base">
<div
v-if="title && (outline.data?.length || allowEdit)"
class="grid grid-cols-[70%,30%] mb-4 px-2"
>
<div class="font-semibold text-lg leading-5">
{{ __(title) }}
</div>
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
{{ __('Add Chapter') }}
</Button>
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">
{{ expandAll ? __("Collapse all chapters") : __("Expand all chapters") }}
</span> -->
</div>
<div
:class="{
'shadow rounded-md pt-2 px-2': showOutline && outline.data?.length,
}"
>
<Disclosure
v-slot="{ open }"
v-for="(chapter, index) in outline.data"
:key="chapter.name"
:defaultOpen="openChapterDetail(chapter.idx)"
>
<DisclosureButton ref="" class="flex w-full p-2">
<ChevronRight
:class="{
'rotate-90 transform duration-200': open,
'duration-200': !open,
open: index == 1,
}"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
<div class="text-base text-left font-medium leading-5">
{{ chapter.title }}
</div>
</DisclosureButton>
<DisclosurePanel>
<Draggable
:list="chapter.lessons"
:disabled="!allowEdit"
item-key="name"
group="items"
@end="updateOutline"
:data-chapter="chapter.name"
>
<template #item="{ element: lesson }">
<div class="outline-lesson pl-8 py-2 pr-4">
<router-link
:to="{
name: allowEdit ? 'LessonForm' : 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
lessonNumber: lesson.number.split('.')[1],
},
}"
>
<div class="flex items-center text-sm leading-5 group">
<MonitorPlay
v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
<HelpCircle
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
{{ lesson.title }}
<Trash2
v-if="allowEdit"
@click.prevent="trashLesson(lesson.name, chapter.name)"
class="h-4 w-4 text-red-500 ml-auto invisible group-hover:visible"
/>
<Check
v-if="lesson.is_complete"
class="h-4 w-4 text-green-700 ml-2"
/>
</div>
</router-link>
</div>
</template>
</Draggable>
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
<router-link
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: chapter.idx,
lessonNumber: chapter.lessons.length + 1,
},
}"
>
<Button>
{{ __('Add Lesson') }}
</Button>
</router-link>
<Button class="ml-2" @click="openChapterModal(chapter)">
{{ __('Edit Chapter') }}
</Button>
</div>
</DisclosurePanel>
</Disclosure>
</div>
</div>
<ChapterModal
v-model="showChapterModal"
v-model:outline="outline"
:course="courseName"
:chapterDetail="getCurrentChapter()"
/>
</template>
<script setup>
import { Button, createResource } from 'frappe-ui'
import { ref, getCurrentInstance } from 'vue'
import Draggable from 'vuedraggable'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import {
ChevronRight,
MonitorPlay,
HelpCircle,
FileText,
Check,
Trash2,
} from 'lucide-vue-next'
import { useRoute } from 'vue-router'
import ChapterModal from '@/components/Modals/ChapterModal.vue'
import { showToast } from '@/utils'
const route = useRoute()
const expandAll = ref(true)
const showChapterModal = ref(false)
const currentChapter = ref(null)
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
const props = defineProps({
courseName: {
type: String,
required: true,
},
showOutline: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
allowEdit: {
type: Boolean,
default: false,
},
getProgress: {
type: Boolean,
default: false,
},
})
const outline = createResource({
url: 'lms.lms.utils.get_course_outline',
cache: ['course_outline', props.courseName],
params: {
course: props.courseName,
progress: props.getProgress,
},
auto: true,
})
const deleteLesson = createResource({
url: 'lms.lms.api.delete_lesson',
makeParams(values) {
return {
lesson: values.lesson,
chapter: values.chapter,
}
},
onSuccess() {
outline.reload()
showToast('Success', 'Lesson deleted successfully', 'check')
},
})
const updateLessonIndex = createResource({
url: 'lms.lms.api.update_lesson_index',
makeParams(values) {
return {
lesson: values.lesson,
sourceChapter: values.sourceChapter,
targetChapter: values.targetChapter,
idx: values.idx,
}
},
onSuccess() {
showToast('Success', 'Lesson moved successfully', 'check')
},
})
const trashLesson = (lessonName, chapterName) => {
$dialog({
title: __('Delete Lesson'),
message: __('Are you sure you want to delete this lesson?'),
actions: [
{
label: __('Delete'),
theme: 'red',
variant: 'solid',
onClick(close) {
deleteLesson.submit({
lesson: lessonName,
chapter: chapterName,
})
close()
},
},
],
})
}
const openChapterDetail = (index) => {
return index == route.params.chapterNumber || index == 1
}
const openChapterModal = (chapter = null) => {
currentChapter.value = chapter
showChapterModal.value = true
}
const getCurrentChapter = () => {
return currentChapter.value
}
const updateOutline = (e) => {
updateLessonIndex.submit({
lesson: e.item.__draggable_context.element.name,
sourceChapter: e.from.dataset.chapter,
targetChapter: e.to.dataset.chapter,
idx: e.newIndex,
})
}
</script>
<style>
.outline-lesson:has(.router-link-active) {
background-color: theme('colors.gray.100');
}
</style>
+115
View File
@@ -0,0 +1,115 @@
<template>
<div v-if="reviews.data?.length || membership" class="mt-20 mb-10">
<Button
v-if="membership && !hasReviewed.data"
@click="openReviewModal()"
class="float-right"
>
{{ __('Write a Review') }}
</Button>
<div class="flex items-center font-semibold text-2xl">
{{ __('Student Reviews') }}
</div>
<div class="grid gap-8 mt-10">
<div v-for="(review, index) in reviews.data">
<div class="flex items-center">
<router-link
:to="{
name: 'Profile',
params: { username: review.owner_details.username },
}"
>
<UserAvatar :user="review.owner_details" :size="'2xl'" />
</router-link>
<div class="mx-4">
<router-link
:to="{
name: 'Profile',
params: { username: review.owner_details.username },
}"
>
<span class="text-lg font-medium mr-4">
{{ review.owner_details.full_name }}
</span>
</router-link>
<span>
{{ review.creation }}
</span>
<div class="flex mt-2">
<Star
v-for="index in 5"
class="h-5 w-5 text-gray-100 bg-gray-200 rounded-sm mr-2"
:class="
index <= Math.ceil(review.rating)
? 'fill-orange-500'
: 'fill-gray-600'
"
/>
</div>
</div>
</div>
<div v-if="review.review" class="mt-4 leading-5">
{{ review.review }}
</div>
</div>
</div>
</div>
<ReviewModal
v-model="showReviewModal"
v-model:reloadReviews="reviews"
v-model:hasReviewed="hasReviewed"
:courseName="courseName"
/>
</template>
<script setup>
import { Star } from 'lucide-vue-next'
import { createResource, Button } from 'frappe-ui'
import { computed, ref, inject } from 'vue'
import UserAvatar from '@/components/UserAvatar.vue'
import ReviewModal from '@/components/Modals/ReviewModal.vue'
const user = inject('$user')
const props = defineProps({
courseName: {
type: String,
required: true,
},
avg_rating: {
type: Number,
required: true,
},
membership: {
type: Object,
required: false,
},
})
const hasReviewed = createResource({
url: 'frappe.client.get_count',
cache: ['eligible_to_review', props.courseName, props.membership?.member],
params: {
doctype: 'LMS Course Review',
filters: {
course: props.courseName,
owner: props.membership?.member,
},
},
auto: user.data?.name ? true : false,
})
const reviews = createResource({
url: 'lms.lms.utils.get_reviews',
cache: ['course_reviews', props.courseName],
params: {
course: props.courseName,
},
auto: true,
})
const showReviewModal = ref(false)
function openReviewModal() {
showReviewModal.value = true
}
</script>
+30
View File
@@ -0,0 +1,30 @@
<template>
<div v-if="course">
<div class="text-xl font-semibold">
{{ course.title }}
</div>
<div v-if="course.chapters.length">
{{ course.chapters }}
</div>
<div v-else class="border bg-white rounded-md p-5 text-center mt-4">
<div>
{{
__(
'There are no chapters in this course. Create and manage chapters from here.'
)
}}
</div>
<Button class="mt-4">
{{ __('Add Chapter') }}
</Button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
course: {
type: Object,
default: {},
},
})
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<div class="relative flex h-full flex-col">
<div class="h-full flex-1">
<div class="flex h-screen text-base">
<div
class="relative block min-h-0 flex-shrink-0 overflow-hidden hover:overflow-auto"
>
<AppSidebar />
</div>
<div class="w-full overflow-auto" id="scrollContainer">
<slot />
</div>
</div>
</div>
</div>
</template>
<script setup>
import AppSidebar from './AppSidebar.vue'
</script>
@@ -0,0 +1,244 @@
<template>
<div class="mt-6">
<div v-if="!singleThread" class="flex items-center mb-5">
<Button variant="outline" @click="showTopics = true">
<template #icon>
<ChevronLeft class="w-5 h-5 stroke-1.5 text-gray-700" />
</template>
</Button>
<span class="text-lg font-semibold ml-2">
{{ topic.title }}
</span>
</div>
<div v-for="(reply, index) in replies.data">
<div
class="py-3"
:class="{ 'border-b': index + 1 != replies.data.length }"
>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<UserAvatar :user="reply.user" class="mr-2" />
<span>
{{ reply.user.full_name }}
</span>
<span class="text-sm ml-2">
{{ timeAgo(reply.creation) }}
</span>
</div>
<Dropdown
v-if="user.data.name == reply.owner && !reply.editable"
:options="[
{
label: 'Edit',
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
onClick() {
deleteReply(reply)
},
},
]"
>
<template v-slot="{ open }">
<MoreHorizontal class="w-4 h-4 stroke-1.5 cursor-pointer" />
</template>
</Dropdown>
<div v-if="reply.editable">
<Button variant="ghost" @click="postEdited(reply)">
{{ __('Post') }}
</Button>
<Button variant="ghost" @click="reply.editable = false">
{{ __('Discard') }}
</Button>
</div>
</div>
<TextEditor
:content="reply.reply"
@change="(val) => (reply.reply = val)"
:editable="reply.editable || false"
:fixedMenu="reply.editable || false"
:editorClass="
reply.editable
? 'ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none'
: 'prose-sm'
"
/>
</div>
</div>
<TextEditor
class="mt-5"
:content="newReply"
:mentions="mentionUsers"
@change="(val) => (newReply = val)"
placeholder="Type your reply here..."
:fixedMenu="true"
editorClass="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none border border-gray-300 rounded-b-md min-h-[7rem] py-1 px-2"
/>
<div class="flex justify-between mt-2">
<span> </span>
<Button @click="postReply()">
<span>
{{ __('Post') }}
</span>
</Button>
</div>
</div>
</template>
<script setup>
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui'
import { timeAgo } from '../utils'
import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted, computed } from 'vue'
import { createToast } from '../utils'
const showTopics = defineModel('showTopics')
const newReply = ref('')
const socket = inject('$socket')
const user = inject('$user')
const allUsers = inject('$allUsers')
const props = defineProps({
topic: {
type: Object,
required: true,
},
singleThread: {
type: Boolean,
default: false,
},
})
onMounted(() => {
socket.on('publish_message', (data) => {
replies.reload()
})
socket.on('update_message', (data) => {
replies.reload()
})
socket.on('delete_message', (data) => {
replies.reload()
})
})
const replies = createResource({
url: 'lms.lms.utils.get_discussion_replies',
cache: ['replies', props.topic],
makeParams(values) {
return {
topic: props.topic.name,
}
},
auto: true,
})
const newReplyResource = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'Discussion Reply',
reply: newReply.value,
topic: props.topic.name,
},
}
},
})
const mentionUsers = computed(() => {
let users = Object.values(allUsers.data).map((user) => {
return {
value: user.name,
label: user.full_name,
}
})
return users
})
const postReply = () => {
newReplyResource.submit(
{},
{
validate() {
if (!newReply.value) {
return 'Reply cannot be empty'
}
},
onSuccess() {
newReply.value = ''
replies.reload()
},
onError(err) {
createToast({
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-red-600 text-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
},
}
)
}
const editReplyResource = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'Discussion Reply',
name: values.name,
fieldname: 'reply',
value: values.reply,
}
},
})
const postEdited = (reply) => {
editReplyResource.submit(
{
name: reply.name,
reply: reply.reply,
},
{
validate() {
if (!reply.reply) {
return 'Reply cannot be empty'
}
},
onSuccess() {
reply.editable = false
replies.reload()
},
}
)
}
const deleteReplyResource = createResource({
url: 'frappe.client.delete',
makeParams(values) {
return {
doctype: 'Discussion Reply',
name: values.name,
}
},
})
const deleteReply = (reply) => {
deleteReplyResource.submit(
{
name: reply.name,
},
{
onSuccess() {
replies.reload()
},
}
)
}
</script>

Some files were not shown because too many files have changed in this diff Show More