Compare commits

...

240 Commits

Author SHA1 Message Date
Raizaaa 0fa4dd7121 Merge pull request #2354 from raizasafeel/chore/dependency
chore: add payments in pyproject for auto install
2026-05-06 08:40:54 +05:30
Raizaaa c2a4ef692f chore: add payments in pyproject for auto install 2026-05-05 14:46:31 +05:30
Raizaaa 6211c507ff Merge pull request #2351 from raizasafeel/chore/translate
chore: add hindi translation
2026-05-03 20:08:22 +05:30
Raizaaa ffbe3d6e69 chore: add hindi translation 2026-05-03 19:53:37 +05:30
Raizaaa 1b13f20ff8 Merge pull request #2347 from frappe/pot_develop_2026-05-01
chore: update POT file
2026-05-03 19:49:32 +05:30
Raizaaa ee9c460f8d Merge pull request #2344 from raizasafeel/security
fix: sanitize lesson blocks from server and client
2026-05-01 21:58:46 +05:30
frappe-pr-bot 6e57e246e9 chore: update POT file 2026-05-01 16:20:17 +00:00
Jannat Patel 5e18ba3c35 Merge pull request #2346 from frappe/readme-payments-app
docs: updated readme to include instructions for payments app
2026-05-01 18:25:18 +05:30
Jannat Patel 3bb7f51295 docs: updated readme to include instructions for payments app 2026-05-01 18:18:01 +05:30
Raizaaa ad037e5f90 fix(lesson): sanitize lesson client side 2026-05-01 02:10:57 +05:30
Raizaaa 07ca95caa8 fix(lesson): sanitize lesson server side 2026-05-01 01:34:05 +05:30
Raizaaa 47b5b603c7 fix(lesson): sanitize lesson server side 2026-05-01 01:15:50 +05:30
Raizaaa eff914c73b Merge pull request #2342 from raizasafeel/fix/patches
fix: sidebar_for_certified_members patch
2026-04-30 15:49:50 +05:30
Raizaaa e3e19c6252 fix: sidebar_for_certified_members patch 2026-04-30 15:40:27 +05:30
Raizaaa e702853b2f Merge pull request #2339 from raizasafeel/chore-deps
chore(deps): update build env deps to frappe >=14, node>=22, apt deps for lxml
2026-04-30 12:58:35 +05:30
Raizaaa 1e7da9b40c chore(deps): update build env deps 2026-04-30 12:49:27 +05:30
Raizaaa ad1f516d66 Merge pull request #2338 from raizasafeel/fix/notification
fix: update notification count on sidebar after they are read
2026-04-30 00:23:47 +05:30
Raizaaa 08794c2424 Merge pull request #2336 from raizasafeel/fix/patches
fix(patches): stale imports breaking bench migrate
2026-04-30 00:10:12 +05:30
Raizaaa 8cff1cf916 fix: render notification count even after sidebar rebuilds 2026-04-29 23:53:38 +05:30
Raizaaa 7fecbe5799 fix: update notification count on sidebar after they are read 2026-04-29 23:51:32 +05:30
Raizaaa 7c46d8bbfc Merge pull request #2337 from raizasafeel/fix/ui-test
test: fix onboarding modal close + cleanup
2026-04-29 23:48:29 +05:30
Raizaaa e25b7c67b9 test: refactor batch and course creation ui tests 2026-04-29 23:38:23 +05:30
Raizaaa 65f4cde5b8 test: fix closeOnboardingModal to reliably close the modal 2026-04-29 21:45:18 +05:30
Raizaaa 56eb556929 fix(patches): stale imports breaking bench migrate 2026-04-29 15:47:45 +05:30
Raizaaa 371b7663e9 Merge pull request #2330 from frappe/pot_develop_2026-04-24
chore: update POT file
2026-04-27 12:30:18 +05:30
frappe-pr-bot aea1059473 chore: update POT file 2026-04-24 16:20:14 +00:00
Raizaaa bb5e277e9e Merge pull request #2325 from raizasafeel/fix/loading-state
fix: show job count on jobs listing for guest users
2026-04-23 00:16:38 +05:30
Raizaaa e4e6f047a4 Merge pull request #2321 from raizasafeel/chapter-progression
feat: on chapter completion continue learning takes you to next chapter
2026-04-23 00:12:13 +05:30
Raizaaa 23996da0bc Merge pull request #2322 from raizasafeel/fix/text-editor
fix: render styles properly in text editor
2026-04-23 00:11:51 +05:30
Raizaaa dc6e3265c0 fix: tab navigation to closed jobs 2026-04-23 00:07:14 +05:30
Raizaaa e3e4b9a648 Merge branch 'frappe:develop' into fix/loading-state 2026-04-23 00:04:39 +05:30
Raizaaa 79d5da75b3 fix: show job count on jobs listing for guest users 2026-04-22 21:55:40 +05:30
Raizaaa 80b802a76b fix: render styles properly in text editor for batchform and courseform 2026-04-21 03:52:42 +05:30
Raizaaa c082f1d30d feat: on chapter completion continue learning takes you to next chapter 2026-04-21 01:55:42 +05:30
Raizaaa 8db8fd489e Merge pull request #2319 from frappe/pot_develop_2026-04-17
chore: update POT file
2026-04-18 17:23:57 +03:00
frappe-pr-bot d94d08f7c3 chore: update POT file 2026-04-17 16:20:30 +00:00
Raizaaa 0fe8d3ac74 Merge pull request #2315 from raizasafeel/fix/types
fix: allow None for optional args in get_order_summary
2026-04-15 10:16:11 +03:00
Raizaaa 193374df47 Merge branch 'frappe:develop' into fix/types 2026-04-15 10:08:09 +03:00
raizasafeel d55951525f fix: allow None for optional args in get_order_summary 2026-04-15 12:37:36 +05:30
Raizaaa 70a9ee1bd7 Merge pull request #2314 from raizasafeel/fix/types
fix: allow None for optional args in get_payment_link
2026-04-15 09:46:35 +03:00
raizasafeel 2878f0232c fix: allow None for optional args in get_payment_link 2026-04-15 12:08:24 +05:30
Raizaaa 189cae3490 Merge pull request #2311 from raizasafeel/fix
fix: loading state on batch details, student enrollment by admin
2026-04-14 14:57:05 +03:00
Raizaaa 2b1df68e78 Merge branch 'frappe:develop' into fix/misc 2026-04-14 14:41:09 +03:00
raizasafeel 46c24b1166 fix: admin roles can enroll students in course/batch successfully 2026-04-14 17:08:13 +05:30
raizasafeel bc139767a8 feat: add empty state to batch student progress detail 2026-04-14 17:07:52 +05:30
Raizaaa 0838a9d325 Merge pull request #2310 from raizasafeel/feat/rtl
feat: rtl support
2026-04-14 13:00:51 +03:00
raizasafeel aed4c6915d refactor: swap remaining physical tailwind classes for rtl-safe equivalents 2026-04-14 11:29:28 +05:30
raizasafeel 07e9b62467 chore: bump frappe-ui to v.0.1.276 for rtl support 2026-04-13 18:10:41 +05:30
raizasafeel 5de889c71b fix: render dayjs import properly 2026-04-13 17:45:53 +05:30
raizasafeel 4278361559 fix: prevent breaking of batch time in rtl 2026-04-13 17:28:17 +05:30
raizasafeel f891f72e20 feat: rtl support for dates 2026-04-13 17:28:04 +05:30
raizasafeel cb5d19e523 Merge remote-tracking branch 'upstream/develop' into feat/rtl 2026-04-13 10:45:10 +05:30
raizasafeel 8b3930169d feat: add rtl support to icons, css styles 2026-04-13 10:10:53 +05:30
raizasafeel 3e05dcedeb feat(lesson): add rtl support 2026-04-13 09:49:05 +05:30
raizasafeel aec9556cf0 feat: sidebar toggle icon adjusts to text direction 2026-04-12 20:55:00 +05:30
raizasafeel c5236b2e50 refactor(components): swap tailwindcss classes with ones with rtl support 2026-04-12 20:54:52 +05:30
raizasafeel fa94e5f96a refactor(pages): swap tailwindcss classes with ones with rtl support 2026-04-12 20:52:22 +05:30
Jannat Patel 88e86e6cfb fix: page length for jobs and certification listing 2026-04-12 11:03:20 +05:30
Jannat Patel 04fe73531f Merge pull request #2308 from pateljannat/misc-issues
refactor: jobs and certified participants view
2026-04-11 22:06:26 +05:30
Jannat Patel dc4f188648 fix: certified participants page height 2026-04-11 20:12:47 +05:30
Jannat Patel 411b400d04 fix: quiz scroll issue in lessons 2026-04-11 19:54:57 +05:30
Jannat Patel ec5e45e6c6 fix: mobile view of lists 2026-04-11 19:50:59 +05:30
Jannat Patel f5b1feade6 Merge pull request #2304 from frappe/pot_develop_2026-04-10
chore: update POT file
2026-04-11 19:50:27 +05:30
frappe-pr-bot 19cb56cb21 chore: update POT file 2026-04-10 16:17:34 +00:00
Jannat Patel af08e6842a fix: list pagination 2026-04-10 19:54:59 +05:30
Jannat Patel c7ccb2d1c5 refactor: certified participants list 2026-04-10 18:06:30 +05:30
Jannat Patel 2ebb6ca745 refactor: job applications list 2026-04-10 16:44:42 +05:30
Jannat Patel 1f040c4561 refactor: jobs list and form ui 2026-04-10 15:26:50 +05:30
Jannat Patel ee85af09ed fix: course import with proper evaluator data 2026-04-10 13:09:20 +05:30
raizasafeel 453862a653 chore(deps): bump frappe-ui to v.0.1.275 for rtl support 2026-04-10 12:57:59 +05:30
raizasafeel ff98df6acd feat: detect language and change direction 2026-04-10 09:57:50 +05:30
Jannat Patel 75ed9c5ec2 fix: removed body content from course export request 2026-04-09 20:29:00 +05:30
Jannat Patel 0f7a2d1975 fix: changed course export request to GET 2026-04-09 20:20:53 +05:30
Raizaaa a4f8497988 Merge pull request #2299 from raizasafeel/security
fix: prevent path traversal in scorm file
2026-04-09 08:55:09 +03:00
Jannat Patel 79d82647ef Merge pull request #2298 from pateljannat/issues-220
fix: misc issues
2026-04-08 17:40:15 +05:30
Raizaaa 4e003a2490 Merge branch 'frappe:develop' into security 2026-04-08 13:10:01 +03:00
Jannat Patel 0d394646d9 fix: removed prefix from assignment and exercise list count 2026-04-08 12:17:10 +05:30
Jannat Patel 071e8dc529 fix: lesson progress after assignment submission 2026-04-08 12:15:23 +05:30
Jannat Patel def3e3d372 fix: mark for review should only be allowed when showing answers is disabled 2026-04-08 11:02:42 +05:30
Jannat Patel 97228e4655 Merge pull request #2296 from pateljannat/issues-219
fix: improved list for assignments and programming exercises
2026-04-07 18:13:25 +05:30
Jannat Patel a507ab425c fix: improved list for assignments and programming exercises 2026-04-07 17:54:03 +05:30
raizasafeel e1b425ed5b fix: prevent path traversal in scorm file 2026-04-06 22:50:37 +05:30
Raizaaa e4ad66c226 Merge pull request #2294 from raizasafeel/security
fix: prevent xss in meta data
2026-04-06 22:45:22 +05:30
Raizaaa 9003e92d6c Merge pull request #2292 from raizasafeel/feat/zero-amount-checkout
fix: allow zero amount checkout with coupons
2026-04-06 20:45:16 +05:30
raizasafeel f244a6c9ff fix: prevent xss in meta data 2026-04-06 20:41:38 +05:30
Jannat Patel 226ed85636 Merge pull request #2293 from pateljannat/issues-218
fix: misc issues
2026-04-06 18:55:12 +05:30
Jannat Patel 717f9000f2 fix: variable names for certificate template 2026-04-06 18:20:24 +05:30
Jannat Patel 0d8898576f fix: learning workspace sidebar 2026-04-06 18:09:10 +05:30
Jannat Patel ab1bed8f30 fix: course card gradient toggle when theme changed 2026-04-06 17:06:44 +05:30
Jannat Patel 93161b8278 fix: sidebar links in mobile 2026-04-06 16:19:52 +05:30
Raizaaa 090f26f58f Merge branch 'frappe:develop' into feat/zero-amount-checkout 2026-04-06 12:12:55 +05:30
raizasafeel 1d04f4fd91 fix: allow zero amount checkout with coupons 2026-04-06 11:58:05 +05:30
Jannat Patel abda48eaad Merge pull request #2278 from iamrubeng/patch-1
ci(build): add payments repository to APPS_JSON
2026-04-06 11:02:09 +05:30
Jannat Patel ad6f24dd7c Merge pull request #2290 from frappe/pot_develop_2026-04-03
chore: update POT file
2026-04-06 10:50:00 +05:30
Raizaaa 2fe39ee2ba Merge pull request #2291 from raizasafeel/fix/ui-teardown
revert: change switches into checkbox
2026-04-06 09:48:41 +05:30
raizasafeel 221ac4fad9 revert: change switches into checkbox 2026-04-06 09:03:53 +05:30
Rubén Gómez 831f119398 build(init): add payments dependency 2026-04-03 19:25:15 +02:00
frappe-pr-bot 540c676206 chore: update POT file 2026-04-03 16:11:34 +00:00
Jannat Patel 90d4f32c47 Merge pull request #2286 from pateljannat/course-package-import
feat: course package import and export
2026-04-03 13:28:15 +05:30
Jannat Patel 7fe8d6c500 fix: asset export path 2026-04-03 13:18:11 +05:30
Jannat Patel 7c1869853f ci: codecov rules 2026-04-03 12:38:34 +05:30
Jannat Patel 3ece2fc3ec fix: assessments replace logic 2026-04-03 12:22:04 +05:30
Jannat Patel f9f17ef8ac fix: roles before user is saved 2026-04-03 11:52:00 +05:30
Jannat Patel a263ca9330 Merge pull request #2288 from Rl0007/fix/configurable-max-signups
fix: take max signups from Frappe settings for custom LMS signup
2026-04-03 11:24:57 +05:30
Jannat Patel ab96e354cc fix: cleanup and more tests 2026-04-03 11:22:40 +05:30
Rahul Agrawal 3d37461a73 fix: Semgrep 2026-04-03 08:25:41 +05:30
Rahul Agrawal b1c68ad4f3 fix: take max signups from Frappe settings for custom LMS signup 2026-04-03 08:16:39 +05:30
Jannat Patel 6338a5911f fix: misc issues 2026-04-02 20:08:08 +05:30
Jannat Patel 6ebaf0e28b test: corrected zip path 2026-04-02 19:24:07 +05:30
Jannat Patel 55f01dc313 test: course package export and import 2026-04-02 18:53:50 +05:30
Jannat Patel c0df21c076 fix: save export zip as private 2026-04-02 17:10:40 +05:30
Jannat Patel 564d10feb6 fix: cleanup of import functionality 2026-04-02 16:48:33 +05:30
Jannat Patel e1e2c08493 fix: import modal ui 2026-04-02 13:52:07 +05:30
Jannat Patel cd85c5c57f fix: import package as private 2026-04-02 13:23:38 +05:30
Jannat Patel 03e5bae0aa chore: resolved conflicts 2026-04-02 12:41:38 +05:30
Jannat Patel a4a0a76ad7 feat: import course zip 2026-04-02 12:40:08 +05:30
Raizaaa 1d2b3b0996 Merge pull request #2282 from raizasafeel/security
fix: prevent unauthorised enrollments in paid courses
2026-04-01 14:32:17 +05:30
raizasafeel c3e3337de4 fix(enrollment): prevent unauthorised enrollments in paid courses 2026-04-01 13:46:10 +05:30
Rubén Gómez 94a80603b0 ci(build): update Frappe branch to version-16 in workflow 2026-04-01 08:57:14 +02:00
Leo Daniel A 42abc678a2 fix(notifications): single-line mobile header and improve empty state (#2250)
* fix(notifications): single-line mobile header and improve empty state

* fix(style): improve the notification content for mobile view and dark theme
2026-03-31 13:01:53 +05:30
Rubén Gómez Soto 78a9eac356 ci(build): add payments repository to APPS_JSON 2026-03-31 08:53:26 +02:00
Jannat Patel 8100c67a00 Merge pull request #2277 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-03-31 11:22:57 +05:30
MochaMind 1c43e6f857 chore: Esperanto translations 2026-03-30 22:17:02 +05:30
MochaMind 46c0e86723 chore: Portuguese, Brazilian translations 2026-03-30 22:17:01 +05:30
MochaMind 21c63722f9 chore: Serbian (Latin) translations 2026-03-30 22:16:59 +05:30
MochaMind f736c896ed chore: Norwegian Bokmal translations 2026-03-30 22:16:57 +05:30
MochaMind 0a732da414 chore: Bosnian translations 2026-03-30 22:16:55 +05:30
MochaMind dc0a4ca45c chore: Burmese translations 2026-03-30 22:16:53 +05:30
MochaMind 399c893028 chore: Croatian translations 2026-03-30 22:16:52 +05:30
MochaMind ff96120cd4 chore: Thai translations 2026-03-30 22:16:50 +05:30
MochaMind c87c21ce7c chore: Persian translations 2026-03-30 22:16:48 +05:30
MochaMind 2a3a5bc875 chore: Indonesian translations 2026-03-30 22:16:47 +05:30
MochaMind d8faa43820 chore: Vietnamese translations 2026-03-30 22:16:46 +05:30
MochaMind 37a63c5771 chore: Chinese Simplified translations 2026-03-30 22:16:44 +05:30
MochaMind 42c88235af chore: Turkish translations 2026-03-30 22:16:43 +05:30
MochaMind eb4f348a4c chore: Swedish translations 2026-03-30 22:16:41 +05:30
MochaMind b35eda205d chore: Serbian (Cyrillic) translations 2026-03-30 22:16:39 +05:30
MochaMind d26c704389 chore: Slovenian translations 2026-03-30 22:16:37 +05:30
MochaMind e6dba195ce chore: Russian translations 2026-03-30 22:16:36 +05:30
MochaMind ed657138dc chore: Portuguese translations 2026-03-30 22:16:34 +05:30
MochaMind 47148353fb chore: Polish translations 2026-03-30 22:16:32 +05:30
MochaMind 9aa8e72dc4 chore: Dutch translations 2026-03-30 22:16:31 +05:30
MochaMind b580b38a04 chore: Italian translations 2026-03-30 22:16:29 +05:30
MochaMind c95662a96c chore: Hungarian translations 2026-03-30 22:16:27 +05:30
MochaMind 15de77c8a6 chore: German translations 2026-03-30 22:16:26 +05:30
MochaMind b80f6bcb1a chore: Danish translations 2026-03-30 22:16:24 +05:30
MochaMind 5845308344 chore: Czech translations 2026-03-30 22:16:23 +05:30
MochaMind b660c81a56 chore: Arabic translations 2026-03-30 22:16:21 +05:30
MochaMind a02cc1e213 chore: Spanish translations 2026-03-30 22:16:20 +05:30
MochaMind 755a69420c chore: French translations 2026-03-30 22:16:18 +05:30
Raizaaa 36c8c291f1 Merge pull request #2274 from raizasafeel/security
fix: prevent path transversals in lms
2026-03-30 15:05:00 +05:30
raizasafeel bb1b1f6adc fix: prevent path transversals in scorm upload 2026-03-30 14:51:16 +05:30
Jannat Patel 0e9abf91a1 Merge pull request #2271 from LeoDanielA01/fix/elevators-route
fix: evaluator navigation by syncing modal visibility with route change
2026-03-30 11:04:59 +05:30
Jannat Patel 0fd5d6b2b0 Merge pull request #2270 from LeoDanielA01/fix/settings-header-text-contrast
fix(style): improve header text contrast in settings pages dark mode
2026-03-30 11:02:20 +05:30
Jannat Patel e926fde159 Merge pull request #2269 from LeoDanielA01/fix/payment-gateway-header-color
fix(style): improve payment gateway header text visibility in dark mode
2026-03-30 10:57:46 +05:30
Jannat Patel 81e8ff5bff Merge pull request #2265 from Owaishk08/fix/sidebar-overflow
fix: remove horizontal scrollbar and duplicate overflow div in sidebar
2026-03-30 10:57:00 +05:30
Jannat Patel c0f5ceacfb Merge pull request #2266 from frappe/pot_develop_2026-03-27
chore: update POT file
2026-03-30 10:51:40 +05:30
LEO DANIEL A 79441ae09c fix: evaluator navigation by syncing modal visibility with route change 2026-03-28 16:43:30 +05:30
LEO DANIEL A 3509da679c fix(style): improve header text contrast in settings pages dark mode 2026-03-28 15:22:23 +05:30
LEO DANIEL A edf3a80d8d fix(style): improve payment gateway header text visibility in dark mode 2026-03-28 15:07:02 +05:30
frappe-pr-bot 0f7322b67d chore: update POT file 2026-03-27 16:17:18 +00:00
Owais Khan c510f28a01 fix: remove horizontal scrollbar and duplicate overflow div in sidebar 2026-03-27 14:43:30 +05:30
Jannat Patel 839c8eca6e Merge pull request #2264 from pateljannat/issues-217
fix: better sanitization of form fields
2026-03-27 12:15:52 +05:30
Jannat Patel ba01c1e803 Merge pull request #2262 from NuriaAmoros/fix/overflow-settings-dialog
fix: add overflow-y-auto to settings dialog content panel (#2079)
2026-03-27 11:53:18 +05:30
Jannat Patel 5efcaab95a fix: better sanitization of form fields 2026-03-27 11:26:04 +05:30
Nuria Amorós 73122d1faa fix: change overflow-y-scroll to overflow-y-auto in settings list components (#2079) 2026-03-26 15:20:46 +01:00
Jannat Patel 26623ecc25 Merge pull request #2260 from pateljannat/issues-216
fix: misc ui improvements
2026-03-26 16:27:24 +05:30
Jannat Patel 79c732e357 Merge pull request #2255 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-03-26 15:59:37 +05:30
Jannat Patel 5b50701b3b fix: ui improvements in batch feedback and evaluator slots 2026-03-26 15:59:02 +05:30
Jannat Patel 71c13d634c fix: enhancements in quiz 2026-03-26 15:58:20 +05:30
Jannat Patel 029d76cf59 fix: demo data roles and email account message 2026-03-26 15:57:36 +05:30
MochaMind 72bc9f9630 chore: Serbian (Latin) translations 2026-03-25 20:57:27 +05:30
MochaMind ce719f8159 chore: Serbian (Cyrillic) translations 2026-03-25 20:57:13 +05:30
MochaMind b62edef938 chore: Spanish translations 2026-03-25 20:57:00 +05:30
Jannat Patel aaa866e3ff feat: export course zip 2026-03-25 16:29:56 +05:30
Jannat Patel 15e9e95129 Merge branch 'main-hotfix' into develop 2026-03-25 11:05:30 +05:30
Hussain Nagaria 924e118d92 Merge pull request #2246 from frappe/misc/fixes
fix: misc issues in batch
2026-03-25 10:33:34 +05:30
Hussain Nagaria e3a70a04a3 Merge branch 'develop' into misc/fixes 2026-03-25 10:23:55 +05:30
Raizaaa 0ec9ad0b26 Merge pull request #2239 from raizasafeel/fix/ui-teardown
style(courseform): render tags inside input to match multiselect
2026-03-25 00:09:47 +05:30
Jannat Patel b4cd463fef Merge pull request #2252 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-03-24 20:52:48 +05:30
MochaMind c4a64c26cd chore: Vietnamese translations 2026-03-24 20:05:34 +05:30
Raizaaa d2d36c75c0 Merge branch 'frappe:develop' into fix/ui-teardown 2026-03-24 16:58:26 +05:30
raizasafeel c2287a5d08 style(courseform): render tags inside input to match multiselect 2026-03-24 16:16:35 +05:30
Raizaaa bcd55984a9 Merge pull request #2247 from raizasafeel/fix/course-progress
fix: course progress updated for scorm and video end event
2026-03-24 16:10:03 +05:30
Hussain Nagaria ecc01825c0 fix: move to correct line 2026-03-24 15:33:58 +05:30
raizasafeel 400c950bb7 fix: progress updated on video completion 2026-03-24 15:33:53 +05:30
Hussain Nagaria 681923e3f7 chore: ignore semgrep on batch details whitelisted method 2026-03-24 15:29:15 +05:30
raizasafeel 89505eac7d fix(scorm): save_progress no longer impended by race condition 2026-03-24 15:27:08 +05:30
Hussain Nagaria e7d2594142 feat: show preview video in batch 2026-03-24 15:19:51 +05:30
Hussain Nagaria 0486842bc8 fix: show error if upload fails 2026-03-24 14:51:09 +05:30
raizasafeel 99397ad1f4 feat(scorm): show completion in frontend 2026-03-24 12:58:44 +05:30
Jannat Patel 847719ab77 Merge pull request #2244 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-03-23 20:07:34 +05:30
MochaMind 7d51da78c9 chore: Esperanto translations 2026-03-23 19:47:15 +05:30
MochaMind 5f9b93280a chore: Portuguese, Brazilian translations 2026-03-23 19:47:13 +05:30
MochaMind 5614c72472 chore: Serbian (Latin) translations 2026-03-23 19:47:11 +05:30
MochaMind 2d3ba826cf chore: Norwegian Bokmal translations 2026-03-23 19:47:09 +05:30
MochaMind 6ca69ecda9 chore: Bosnian translations 2026-03-23 19:47:08 +05:30
MochaMind 8ff9cde1e3 chore: Burmese translations 2026-03-23 19:47:06 +05:30
MochaMind 9fa73ecca2 chore: Croatian translations 2026-03-23 19:47:04 +05:30
MochaMind fdc019c106 chore: Thai translations 2026-03-23 19:47:03 +05:30
MochaMind f4eff5d088 chore: Persian translations 2026-03-23 19:47:01 +05:30
MochaMind ea18d07baf chore: Indonesian translations 2026-03-23 19:46:59 +05:30
MochaMind ffd7d0e466 chore: Vietnamese translations 2026-03-23 19:46:57 +05:30
MochaMind 080dbdf9cd chore: Chinese Simplified translations 2026-03-23 19:46:55 +05:30
MochaMind 9533ba3b76 chore: Turkish translations 2026-03-23 19:46:54 +05:30
MochaMind 17ebc7ae4f chore: Swedish translations 2026-03-23 19:46:52 +05:30
MochaMind b2616817e5 chore: Serbian (Cyrillic) translations 2026-03-23 19:46:50 +05:30
MochaMind 32fe61b965 chore: Slovenian translations 2026-03-23 19:46:48 +05:30
MochaMind 4e92c700bb chore: Russian translations 2026-03-23 19:46:47 +05:30
MochaMind f1e5ce4499 chore: Portuguese translations 2026-03-23 19:46:45 +05:30
MochaMind 410f06b2a2 chore: Polish translations 2026-03-23 19:46:44 +05:30
MochaMind 4b701e5638 chore: Dutch translations 2026-03-23 19:46:42 +05:30
MochaMind 8919f8933a chore: Italian translations 2026-03-23 19:46:40 +05:30
MochaMind 3617dd04e9 chore: Hungarian translations 2026-03-23 19:46:39 +05:30
MochaMind 1496add6e4 chore: German translations 2026-03-23 19:46:37 +05:30
MochaMind b8a0105d85 chore: Danish translations 2026-03-23 19:46:36 +05:30
MochaMind 3f57a18b3f chore: Czech translations 2026-03-23 19:46:34 +05:30
MochaMind c65e38fd1b chore: Arabic translations 2026-03-23 19:46:32 +05:30
MochaMind 8737b29475 chore: Spanish translations 2026-03-23 19:46:30 +05:30
MochaMind c25a8896ce chore: French translations 2026-03-23 19:46:29 +05:30
Nuria Amorós 4317c2297c fix: add overflow-y-auto to settings dialog content panel (#2079) 2026-03-23 11:37:26 +01:00
Jannat Patel 6e852cb86f Merge pull request #2238 from LeoDanielA01/fix/job-detail-application-data-loading
fix(JobDetail): application count and button now load properly on pag…
2026-03-23 11:09:32 +05:30
Jannat Patel 2f468ea0ec Merge pull request #2237 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-03-23 11:06:31 +05:30
Jannat Patel 9bb9177d36 Merge pull request #2235 from frappe/pot_develop_2026-03-20
chore: update POT file
2026-03-23 11:05:49 +05:30
LEO DANIEL A f92b7faf0f fix(JobDetail): application count and button now load properly on page open 2026-03-22 22:15:58 +05:30
MochaMind e94fd2949d chore: Hungarian translations 2026-03-22 19:48:39 +05:30
MochaMind e1ac29d79f chore: Arabic translations 2026-03-22 19:48:34 +05:30
frappe-pr-bot f559fa1b32 chore: update POT file 2026-03-20 16:13:38 +00:00
Jannat Patel 46ee36a6dc Merge pull request #2233 from pateljannat/issues-215
fix: misc issues
2026-03-20 20:20:52 +05:30
Jannat Patel ca1b5da8e5 fix: improved role description in profile 2026-03-19 18:19:07 +05:30
Jannat Patel 189fc08cdb revert: certification batch filter to a checkbox 2026-03-19 18:01:09 +05:30
Leo Daniel A f2807d3e38 fix: LiveCodeSession reference error and UI loading state (#2218)
* fix: LiveCodeSession reference error and UI loading state

* fix: remove trailing slash from livecode_url in LMS Settings and add patch to update existing records

* fix: remove reload function in patch file and update live_code_url default value by administrator

---------

Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com>
2026-03-19 17:55:24 +05:30
Jannat Patel 9b8721fa87 ci: run ui tests parallely (#2232)
* ci: run ui tests parallely

* ci: run ui tests parallely

* ci: run ui tests parallely
2026-03-19 17:20:48 +05:30
Jannat Patel 1e646e35a2 Merge pull request #2228 from raizasafeel/fix/ui-teardown
revert(course): use checkbox with tooltip for certification filter
2026-03-19 16:18:22 +05:30
Jannat Patel 7730b58a02 Merge pull request #2229 from pateljannat/issues-213
fix: misc issues
2026-03-19 15:52:49 +05:30
Jannat Patel 8f4bd7afaf fix: misc ui issues 2026-03-19 15:38:31 +05:30
Jannat Patel 0d39f1cce1 fix: certification should be visible by default in sidebar 2026-03-19 15:37:51 +05:30
Jannat Patel e18d27e9de fix: events permission to moderator and evaluator 2026-03-19 15:37:18 +05:30
Raizaaa 52e44bee12 Merge branch 'frappe:develop' into fix/ui-teardown 2026-03-19 13:35:24 +05:30
raizasafeel 5c03754888 revert(course): use checkbox with tooltip for certification filter 2026-03-19 13:34:52 +05:30
200 changed files with 34954 additions and 16239 deletions
+3 -3
View File
@@ -38,9 +38,9 @@ jobs:
- name: Set Branch
run: |
export APPS_JSON='[{"url": "https://github.com/frappe/lms","branch": "main"}]'
export APPS_JSON='[{"url": "https://github.com/frappe/payments","branch": "version-15"},{"url": "https://github.com/frappe/lms","branch": "main"}]'
echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
echo "FRAPPE_BRANCH=version-15" >> $GITHUB_ENV
echo "FRAPPE_BRANCH=version-16" >> $GITHUB_ENV
- name: Set Image Tag
run: |
@@ -61,4 +61,4 @@ jobs:
ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }}
build-args: |
"FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}"
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
+13 -3
View File
@@ -1,4 +1,4 @@
name: UI
name: UI Tests
on:
pull_request:
@@ -16,13 +16,14 @@ permissions:
jobs:
test:
name: UI Tests (Cypress) - ${{ matrix.containers }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
name: UI Tests (Cypress)
matrix:
containers: [1, 2]
services:
mariadb:
@@ -114,6 +115,15 @@ jobs:
env:
CYPRESS_BASE_URL: http://lms.test:8000
CYPRESS_RECORD_KEY: 095366ec-7b9f-41bd-aeec-03bb76d627fe
SPLIT: ${{ strategy.job-total }}
SPLIT_INDEX: ${{ strategy.job-index }}
- name: Upload Cypress screenshots if tests fail
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: cypress-screenshots-${{ matrix.containers }}
path: cypress/screenshots
- name: Stop server and wait for coverage file
run: |
+13 -5
View File
@@ -152,11 +152,19 @@ You need Docker, docker-compose and git setup on your machine. Refer [Docker doc
To setup the repository locally follow the steps mentioned below:
1. Install bench and setup a `frappe-bench` directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation)
1. Start the server by running `bench start`
1. In a separate terminal window, create a new site by running `bench new-site learning.test`
1. Map your site to localhost with the command `bench --site learning.test add-to-hosts`
1. Get the Learning app. Run `bench get-app https://github.com/frappe/lms`
1. Run `bench --site learning.test install-app lms`.
1. Start the server by running
```sh
$ bench start
```
1. In a separate terminal window, run the following commands.
```sh
$ bench new-site learning.test
$ bench --site learning.test add-to-hosts
$ bench get-app https://github.com/frappe/payments
$ bench get-app https://github.com/frappe/lms
$ bench --site learning.test install-app lms
```
1. Now open the URL `http://learning.test:8000/lms` in your browser, you should see the app running
## Learn and connect
+7
View File
@@ -1,2 +1,9 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
ignore:
- "**/test_helper.py"
+8
View File
@@ -1,4 +1,5 @@
import { defineConfig } from "cypress";
import cypressSplit from "cypress-split";
export default defineConfig({
projectId: "vandxn",
@@ -14,5 +15,12 @@ export default defineConfig({
},
e2e: {
baseUrl: "http://pertest:8000",
setupNodeEvents(on, config) {
// Splitting tests only works when Cypress Cloud is not orchestrating parallel runs.
if (process.env.CYPRESS_CLOUD_PARALLEL !== "1") {
cypressSplit(on, config);
}
return config;
},
},
});
+21 -11
View File
@@ -7,7 +7,7 @@ describe("Batch Creation", () => {
// Open Settings
cy.get("span").contains("Learning").click();
cy.get("span").contains("Settings").click();
cy.contains('[role="menuitem"]', "Settings").click();
// Add a new member
cy.get("[data-dismissable-layer]")
@@ -38,7 +38,7 @@ describe("Batch Creation", () => {
.find("button")
.contains("New")
.click();
cy.get("span").contains("New Evaluator").click();
cy.contains('[role="menuitem"]', "New Evaluator").click();
const randomEvaluator = `evaluator${dateNow}@example.com`;
cy.get("input[placeholder='jane@doe.com']").type(randomEvaluator);
@@ -52,7 +52,7 @@ describe("Batch Creation", () => {
// Create a batch
cy.get("button").contains("Create").click();
cy.get("span").contains("New Batch").click();
cy.contains('[role="menuitem"]', "New Batch").click();
cy.wait(500);
cy.get("label").contains("Title").type("Test Batch");
cy.get("label").contains("Start Date").type("2030-10-01");
@@ -65,7 +65,7 @@ describe("Batch Creation", () => {
cy.get("label")
.contains("Description")
.type("Test Batch Short Description to test the UI");
cy.get("div[contenteditable=true").invoke(
cy.get("div.ProseMirror").invoke(
"text",
"Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
@@ -74,7 +74,7 @@ describe("Batch Creation", () => {
.contains("Instructors")
.parent()
.within(() => {
cy.get("input").click().type("evaluator");
cy.get("input").click().clear().type(randomEvaluator);
cy.get("input")
.invoke("attr", "aria-controls")
.as("instructor_list_id");
@@ -87,7 +87,20 @@ describe("Batch Creation", () => {
});
});
cy.button("Save").click();
cy.get("label").contains("Published").click();
cy.wait(1000);
// going to batch settings and publishing the batch
cy.url().should("include", "#settings");
cy.closeOnboardingModal();
cy.contains("label", "Published")
.invoke("attr", "for")
.then((id) => {
cy.get(`#${id}`)
.scrollIntoView()
.should("be.visible")
.click({ force: true });
cy.get(`#${id}`).should("have.attr", "aria-checked", "true");
});
cy.button("Save").click();
cy.wait(1000);
let batchName;
@@ -105,11 +118,7 @@ describe("Batch Creation", () => {
cy.url().should("include", "/lms/batches");
cy.get('[id^="headlessui-radiogroup-v-"]')
.find("span")
.contains("Upcoming")
.should("be.visible")
.click();
cy.contains('[role="radio"]', "Upcoming").should("be.visible").click();
cy.get("@batchName").then((batchName) => {
cy.get(`a[href='/lms/batches/${batchName}'`).within(() => {
@@ -154,6 +163,7 @@ describe("Batch Creation", () => {
cy.get("button:visible").contains("Dashboard").click();
/* Add student to batch */
cy.closeOnboardingModal();
cy.get("button").contains("Enroll").click();
cy.get('div[role="dialog"]')
.first()
+9 -6
View File
@@ -9,14 +9,14 @@ describe("Course Creation", () => {
// Create a course
cy.get("button").contains("Create").click();
cy.get("span").contains("New Course").click();
cy.contains('[role="menuitem"]', "New Course").click();
cy.wait(500);
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(
cy.get("div.ProseMirror").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."
);
@@ -61,7 +61,7 @@ describe("Course Creation", () => {
});
cy.button("Save").last().click();
cy.closeOnboardingModal();
// Edit Course Details
cy.wait(500);
cy.get("label")
@@ -153,7 +153,7 @@ describe("Course Creation", () => {
cy.wait(500);
cy.get("[data-dismissable-layer]").within(() => {
cy.get("label").contains("Title").type("Test Discussion");
cy.get("div[contenteditable=true]").invoke(
cy.get("div.ProseMirror").invoke(
"text",
"This is a test discussion. This will check if the UI is working properly."
);
@@ -163,7 +163,7 @@ describe("Course Creation", () => {
// View Discussion
cy.wait(500);
cy.get("div").contains("Test Discussion").click();
cy.get("div[contenteditable=true").invoke(
cy.get("div.ProseMirror").invoke(
"text",
"This is a test comment. This will check if the UI is working properly."
);
@@ -176,7 +176,10 @@ describe("Course Creation", () => {
cy.get("div").contains("Test Course").click();
cy.get("button").contains("Settings").click();
cy.get("header").within(() => {
cy.get("svg.lucide.lucide-trash2-icon").click();
cy.get("svg.lucide.lucide-ellipsis-icon").click();
});
cy.get("div[role=menu]").within(() => {
cy.contains('[role="menuitem"]', "Delete").click();
});
cy.get("span").contains("Delete").click();
cy.wait(500);
+12 -8
View File
@@ -72,15 +72,19 @@ Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => {
Cypress.Commands.add("closeOnboardingModal", () => {
cy.wait(500);
const modalSelector = '[data-testid="onboarding-help-modal"]';
cy.get("body").then(($body) => {
// Check if any element with class including 'z-50' exists
if ($body.find('[class*="z-50"]').length > 0) {
cy.get('[class*="z-50"]')
.find('button:has(svg[class*="feather-x"])')
.realClick();
cy.wait(1000);
} else {
cy.log("Onboarding modal not found, skipping close.");
if (!$body.find(modalSelector).length) {
cy.log("Onboarding modal not present, skipping close.");
return;
}
// Skip onboarding steps if the button exists, otherwise just close the modal.
if ($body.find(`${modalSelector} button:contains("Skip all")`).length) {
cy.get(modalSelector).contains("button", "Skip all").click();
}
cy.get(modalSelector).find("button:has(svg.feather-x)").click();
cy.get(modalSelector).should("not.exist");
});
});
+2
View File
@@ -24,6 +24,7 @@ bench set-redis-socketio-host redis://redis:6379
sed -i '/redis/d' ./Procfile
sed -i '/watch/d' ./Procfile
bench get-app payments
bench get-app lms
bench new-site lms.localhost \
@@ -32,6 +33,7 @@ bench new-site lms.localhost \
--admin-password admin \
--no-mariadb-socket
bench --site lms.localhost install-app payments
bench --site lms.localhost install-app lms
bench --site lms.localhost set-config developer_mode 1
bench --site lms.localhost clear-cache
+15 -15
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ boot.lang }}" dir="{{ boot.text_direction }}">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="{{ favicon }}" />
@@ -201,26 +201,26 @@
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</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 }}" />
<title>{{ title | e }}</title>
<meta name="title" content="{{ meta.title | e }}" />
<meta name="image" content="{{ meta.image | e }}" />
<meta name="description" content="{{ meta.description | e }}" />
<meta name="keywords" content="{{ meta.keywords | e }}" />
<meta property="og:title" content="{{ meta.title | e }}" />
<meta property="og:image" content="{{ meta.image | e }}" />
<meta property="og:description" content="{{ meta.description | e }}" />
<meta name="twitter:title" content="{{ meta.title | e }}" />
<meta name="twitter:image" content="{{ meta.image | e }}" />
<meta name="twitter:description" content="{{ meta.description | e }}" />
</head>
<body class="sm:overscroll-y-none no-scrollbar">
<div id="app">
<div id="seo-content">
<h1>{{ meta.title }}</h1>
<h1>{{ meta.title | e }}</h1>
<p>
{{ meta.description }}
{{ meta.description | e }}
</p>
<a href="{{ meta.link }}">Know More</a>
<a href="{{ meta.link | e }}">Know More</a>
</div>
</div>
<script>
+2 -2
View File
@@ -31,7 +31,7 @@
"dayjs": "1.11.10",
"dompurify": "3.2.6",
"feather-icons": "4.28.0",
"frappe-ui": "^0.1.264",
"frappe-ui": "^0.1.276",
"highlight.js": "11.11.1",
"lucide-vue-next": "0.383.0",
"markdown-it": "14.0.0",
@@ -49,7 +49,7 @@
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.0.3",
"@vitejs/plugin-vue": "5.0.3",
"autoprefixer": "10.4.2",
"postcss": "8.4.5",
"tailwindcss": "^3.4.15",
+35 -26
View File
@@ -5,7 +5,7 @@
:class="{ 'border rounded-lg overflow-auto': !showTitle }"
>
<div
class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]"
class="border-e p-5 overflow-y-auto h-[calc(100vh-3.2rem)]"
:class="{ 'h-full': !showTitle }"
>
<div v-if="showTitle" class="text-lg font-semibold mb-5 text-ink-gray-9">
@@ -17,7 +17,7 @@
</div>
</div>
<div class="text-ink-gray-9 font-semibold mb-5">
{{ __('Assignment Question') }}
{{ __('Assignment') }}: {{ assignment.data.title }}
</div>
<div
v-html="assignment.data.question"
@@ -31,7 +31,7 @@
<div class="font-semibold text-ink-gray-9">
{{ __('Submission') }}
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<Badge v-if="isDirty" theme="orange">
{{ __('Not Saved') }}
</Badge>
@@ -106,7 +106,7 @@
class="cursor-pointer !no-underline text-sm leading-5"
>
<div class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<div class="border rounded-md p-2 me-2">
<FileText class="h-5 w-5 stroke-1.5" />
</div>
<span>
@@ -117,7 +117,7 @@
<X
v-if="canModifyAssignment"
@click="removeSubmission()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ms-4"
/>
</div>
</div>
@@ -300,7 +300,7 @@ const submitAssignment = () => {
}
}
const addNewSubmission = () => {
const prepareSubmissionDoc = () => {
let doc = {
doctype: 'LMS Assignment Submission',
assignment: props.assignmentID,
@@ -311,24 +311,31 @@ const addNewSubmission = () => {
} else {
doc.assignment_attachment = attachment.value
}
return doc
}
const addNewSubmission = () => {
let doc = prepareSubmissionDoc()
if (!doc.assignment_attachment && !doc.answer) {
toast.error(
__('Please provide an answer or upload a file before submitting.')
)
return
}
call('frappe.client.insert', {
doc: doc,
})
.then((data) => {
toast.success(__('Assignment submitted successfully'))
if (router.currentRoute.value.name == 'AssignmentSubmission') {
router.push({
name: 'AssignmentSubmission',
params: {
assignmentID: props.assignmentID,
submissionName: data.name,
},
query: { fromLesson: router.currentRoute.value.query.fromLesson },
})
} else {
markLessonProgress()
router.go()
}
router.push({
name: 'AssignmentSubmission',
params: {
assignmentID: props.assignmentID,
submissionName: data.name,
},
query: { fromLesson: router.currentRoute.value.query.fromLesson },
})
markLessonProgress()
isDirty.value = false
submissionResource.name = data.name
submissionResource.reload()
@@ -372,15 +379,17 @@ const saveSubmission = (file) => {
}
const markLessonProgress = () => {
if (router.currentRoute.value.name == 'Lesson') {
let courseName = router.currentRoute.value.params.courseName
let chapterNumber = router.currentRoute.value.params.chapterNumber
let lessonNumber = router.currentRoute.value.params.lessonNumber
let pathname = window.location.pathname.split('/')
if (!pathname.includes('courses'))
pathname = window.parent.location.pathname.split('/')
if (pathname[2] != 'courses') return
let lessonIndex = pathname.pop().split('-')
if (lessonIndex.length == 2) {
call('lms.lms.api.mark_lesson_progress', {
course: courseName,
chapter_number: chapterNumber,
lesson_number: lessonNumber,
course: pathname[3],
chapter_number: lessonIndex[0],
lesson_number: lessonIndex[1],
})
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<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">
<div class="flex items-center gap-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-ink-gray-9" />
@@ -2,13 +2,13 @@
<Dialog v-model="show" :options="{ size: '2xl' }">
<template #body>
<div class="text-base">
<div class="flex items-center space-x-2 pl-4.5 border-b">
<div class="flex items-center gap-x-2 ps-4.5 border-b">
<Search class="size-4 text-ink-gray-4" />
<input
ref="inputRef"
type="text"
placeholder="Search"
class="w-full border-none bg-transparent py-3 !pl-2 pr-4.5 text-base text-ink-gray-7 placeholder-ink-gray-4 focus:ring-0"
class="w-full border-none bg-transparent py-3 !ps-2 pe-4.5 text-base text-ink-gray-7 placeholder-ink-gray-4 focus:ring-0"
@input="onInput"
v-model="query"
autocomplete="off"
@@ -32,9 +32,9 @@
</div>
<div
class="flex items-center space-x-5 w-full border-t py-2 text-sm text-ink-gray-7 px-4.5"
class="flex items-center gap-x-5 w-full border-t py-2 text-sm text-ink-gray-7 px-4.5"
>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<MoveUp
class="size-5 stroke-1.5 bg-surface-gray-2 p-1 rounded-sm"
/>
@@ -45,7 +45,7 @@
{{ __('to navigate') }}
</span>
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<CornerDownLeft
class="size-5 stroke-1.5 bg-surface-gray-2 p-1 rounded-sm"
/>
@@ -53,7 +53,7 @@
{{ __('to select') }}
</span>
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<span class="bg-surface-gray-2 p-1 rounded-sm"> esc </span>
<span>
{{ __('to close') }}
@@ -10,7 +10,7 @@
:class="{ 'bg-surface-gray-2': item.isActive }"
@click="emit('navigateTo', item.route)"
>
<div class="flex items-center space-x-3">
<div class="flex items-center gap-x-3">
<component
v-if="item.icon"
:is="item.icon"
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="flex items-center text-ink-gray-7">
<Calendar class="h-4 w-4 stroke-1.5 mr-2" />
<Calendar class="h-4 w-4 stroke-1.5 me-2" />
<span>
{{ getFormattedDateRange(props.startDate, props.endDate) }}
</span>
+1 -1
View File
@@ -28,7 +28,7 @@
</div>
</template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<div class="pb-5 float-end">
<Button variant="solid" @click="sendMail(close)">
{{ __('Send') }}
</Button>
@@ -65,7 +65,7 @@
placeholder="Search"
/>
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
class="absolute end-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="selectedValue = null"
>
<X class="h-4 w-4 stroke-1.5 text-ink-gray-7" />
@@ -6,7 +6,7 @@
<div class="overflow-visible border border-outline-gray-modals rounded-md">
<div class="overflow-x-auto">
<div
class="grid items-center space-x-4 p-2 border-b border-outline-gray-modals"
class="grid items-center gap-x-4 p-2 border-b border-outline-gray-modals"
:style="{ gridTemplateColumns: getGridTemplateColumns() }"
>
<div
@@ -21,7 +21,7 @@
<div
v-for="(row, rowIndex) in rows"
:key="rowIndex"
class="grid items-center space-x-4 p-2"
class="grid items-center gap-x-4 p-2"
:style="{ gridTemplateColumns: getGridTemplateColumns() }"
>
<template v-for="key in Object.keys(row)" :key="key">
@@ -47,7 +47,7 @@
<div
v-if="menuOpenIndex === rowIndex"
ref="menuRef"
class="absolute right-0 w-32 z-50 bg-surface-modal border border-outline-gray-modals rounded-md shadow-sm"
class="absolute end-0 w-32 z-50 bg-surface-modal border border-outline-gray-modals rounded-md shadow-sm"
:class="
rowIndex == (rows?.length ?? 0) - 1
? 'bottom-full mb-1'
@@ -56,7 +56,7 @@
>
<button
@click="deleteRow(rowIndex)"
class="flex items-center space-x-2 w-full text-left px-3 py-2 text-sm text-ink-red-3"
class="flex items-center gap-x-2 w-full text-start px-3 py-2 text-sm text-ink-red-3"
>
<Trash2 class="size-4 stroke-1.5" />
<span>
@@ -8,7 +8,7 @@
<template #target="{ togglePopover }">
<button
@click="openPopover(togglePopover)"
class="flex w-full items-center space-x-2 focus:outline-none bg-surface-gray-2 rounded h-7 py-1.5 px-2 hover:bg-surface-gray-3 focus:bg-surface-white border border-gray-100 hover:border-outline-gray-modals focus:border-outline-gray-4"
class="flex w-full items-center gap-x-2 focus:outline-none bg-surface-gray-2 rounded h-7 py-1.5 px-2 hover:bg-surface-gray-3 focus:bg-surface-white border border-gray-100 hover:border-outline-gray-modals focus:border-outline-gray-4"
>
<component
v-if="selectedIcon"
@@ -14,7 +14,7 @@
v-for="value in values"
:key="value"
type="button"
class="inline-flex items-center gap-1 bg-surface-white border border-outline-gray-2 text-ink-gray-7 pl-2 pr-1.5 py-0.5 rounded text-base leading-5"
class="inline-flex items-center gap-1 bg-surface-white border border-outline-gray-2 text-ink-gray-7 ps-2 pe-1.5 py-0.5 rounded text-base leading-5"
@click.stop="removeValue(value)"
>
<span>{{ value }}</span>
+1 -1
View File
@@ -10,7 +10,7 @@
@mouseleave="hoveredRating = 0"
>
<Star
class="fill-gray-400 text-gray-50 stroke-1 mr-1 cursor-pointer"
class="fill-gray-400 text-gray-50 stroke-1 me-1 cursor-pointer"
:class="iconClasses(index)"
@click="markRating(index)"
/>
+16 -5
View File
@@ -9,6 +9,7 @@
:fileTypes="[fileType]"
:validateFile="(file: File) => validateFile(file, true, type)"
@success="(file: File) => saveFile(file)"
@failure="onUploadFailure"
>
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="flex items-center">
@@ -18,9 +19,9 @@
class="size-5 stroke-1 text-ink-gray-7"
/>
</div>
<div class="ml-4">
<Button @click="openFileSelector">
{{ __('Upload') }}
<div class="ms-4">
<Button @click="openFileSelector" :loading="uploading">
{{ uploading ? `${__('Uploading')} ${progress}%` : __('Upload') }}
</Button>
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{ __(description) }}
@@ -45,7 +46,7 @@
<source :src="modelValue" />
{{ __('Your browser does not support the video tag.') }}
</video>
<div class="ml-4">
<div class="ms-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
@@ -62,7 +63,7 @@
</template>
<script setup lang="ts">
import { validateFile } from '@/utils'
import { Button, FileUploader } from 'frappe-ui'
import { Button, FileUploader, toast } from 'frappe-ui'
import { Image, Video } from 'lucide-vue-next'
import { computed } from 'vue'
@@ -100,4 +101,14 @@ const saveFile = (file: any) => {
const removeImage = () => {
emit('update:modelValue', '')
}
const onUploadFailure = (error: any) => {
let message = __('Error Uploading File')
if (error?._server_messages) {
message = JSON.parse(JSON.parse(error._server_messages)[0]).message
} else if (error?.exc) {
message = JSON.parse(error.exc)[0].split('\n').slice(-2, -1)[0]
}
toast.error(message)
}
</script>
+16 -14
View File
@@ -1,7 +1,7 @@
<template>
<div
v-if="course.title"
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9"
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9 bg-surface-cards"
style="min-height: 350px"
>
<div
@@ -10,7 +10,7 @@
course.image
? { backgroundImage: `url('${encodeURI(course.image)}')` }
: {
backgroundImage: getGradientColor(),
backgroundImage: gradientColor,
backgroundBlendMode: 'screen',
}
"
@@ -18,7 +18,7 @@
<!-- <div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<div
v-if="course.featured"
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white border border-outline-amber-1 px-2 py-0.5 rounded-md mr-1 mb-1"
class="flex items-center gap-x-1 text-xs text-ink-amber-3 bg-surface-white border border-outline-amber-1 px-2 py-0.5 rounded-md me-1 mb-1"
>
<Star class="size-3 stroke-2" />
<span>
@@ -28,7 +28,7 @@
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="text-xs border bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 mr-1"
class="text-xs border bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 me-1"
>
{{ tag }}
</div>
@@ -52,7 +52,7 @@
<div v-if="course.lessons">
<Tooltip :text="__('Lessons')">
<span class="flex items-center">
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
<BookOpen class="h-4 w-4 stroke-1.5 me-1" />
{{ course.lessons }}
</span>
</Tooltip>
@@ -61,7 +61,7 @@
<div v-if="course.enrollments">
<Tooltip :text="__('Enrolled Students')">
<span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 mr-1" />
<Users class="h-4 w-4 stroke-1.5 me-1" />
{{ formatAmount(course.enrollments) }}
</span>
</Tooltip>
@@ -70,7 +70,7 @@
<div v-if="course.rating">
<Tooltip :text="__('Average Rating')">
<span class="flex items-center">
<Star class="h-4 w-4 stroke-1.5 mr-1" />
<Star class="h-4 w-4 stroke-1.5 me-1" />
{{ course.rating }}
</span>
</Tooltip>
@@ -105,7 +105,7 @@
<div class="flex items-center justify-between mt-auto">
<div class="flex avatar-group overlap">
<div
class="h-6 mr-1"
class="h-6 me-1"
:class="{ 'avatar-group overlap': course.instructors.length > 1 }"
>
<UserAvatar
@@ -116,7 +116,7 @@
<CourseInstructors :instructors="course.instructors" />
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
@@ -137,6 +137,8 @@ import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { Tooltip } from 'frappe-ui'
import { formatAmount } from '@/utils'
import { theme } from '@/utils/theme'
import { computed, watch } from 'vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import ProgressBar from '@/components/ProgressBar.vue'
@@ -151,12 +153,12 @@ const props = defineProps({
},
})
const getGradientColor = () => {
let theme = localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
const gradientColor = computed(() => {
let themeMode = theme.value === 'dark' ? 'darkMode' : 'lightMode'
let color = props.course.card_gradient?.toLowerCase() || 'blue'
let colorMap = colors[theme][color]
let colorMap = colors[themeMode][color]
return `linear-gradient(to top right, black, ${colorMap[400]})`
}
})
</script>
<style>
.course-card-pills {
@@ -182,7 +184,7 @@ const getGradientColor = () => {
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
margin-inline-start: calc(-8px);
}
.short-introduction {
@@ -96,14 +96,14 @@
</div>
<div class="flex items-center text-ink-gray-9">
<BookOpen class="h-4 w-4 stroke-1.5" />
<span class="ml-2">
<span class="ms-2">
{{ course.data.lessons }}
{{ course.data.lessons > 1 ? __('lessons') : __('lesson') }}
</span>
</div>
<div class="flex items-center text-ink-gray-9">
<Users class="h-4 w-4 stroke-1.5" />
<span class="ml-2">
<span class="ms-2">
{{ formatAmount(course.data.enrollments) }}
{{
course.data.enrollments > 1
@@ -117,7 +117,7 @@
class="flex items-center text-ink-gray-9"
>
<Star class="size-4 stroke-1.5 fill-yellow-500 text-transparent" />
<span class="ml-2">
<span class="ms-2">
{{ course.data.rating }} {{ __('average rating') }}
</span>
</div>
@@ -126,7 +126,7 @@
class="flex items-center font-semibold text-ink-gray-9"
>
<GraduationCap class="h-4 w-4 stroke-2" />
<span class="ml-2">
<span class="ms-2">
{{ __('Certificate of Completion') }}
</span>
</div>
@@ -135,7 +135,7 @@
class="flex items-center font-semibold text-ink-gray-9"
>
<GraduationCap class="h-4 w-4 stroke-2" />
<span class="ml-2">
<span class="ms-2">
{{ __('Paid Certificate after Evaluation') }}
</span>
</div>
+25 -15
View File
@@ -2,7 +2,7 @@
<div class="">
<div
v-if="title && (outline.data?.length || allowEdit)"
class="flex items-center justify-between space-x-2 mb-4 px-2"
class="flex items-center justify-between gap-x-2 mb-4 px-2"
:class="{
'sticky top-0 z-10 bg-surface-white border-b px-3 py-2.5 sm:px-5':
allowEdit,
@@ -46,20 +46,20 @@
>
<ChevronRight
:class="{
'rotate-90 transform duration-200': open,
'duration-200': !open,
'rotate-90': open,
'rtl:rotate-180': !open,
hidden: chapter.is_scorm_package,
open: index == 1,
}"
class="h-4 w-4 text-ink-gray-9 stroke-1"
class="h-4 w-4 text-ink-gray-9 stroke-1 transform duration-200"
/>
<div
class="text-base text-left text-ink-gray-9 font-medium leading-5 ml-2"
class="text-base text-start text-ink-gray-9 font-medium leading-5 ms-2"
@click="redirectToChapter(chapter)"
>
{{ chapter.title }}
</div>
<div class="flex ml-auto space-x-4">
<div class="flex ms-auto gap-x-4">
<Tooltip :text="__('Edit Chapter')" placement="bottom">
<FilePenLine
v-if="allowEdit"
@@ -75,6 +75,12 @@
/>
</Tooltip>
</div>
<Check
v-if="
chapter.is_scorm_package && isScormChapterComplete(chapter)
"
class="h-4 w-4 text-green-700"
/>
</DisclosureButton>
<DisclosurePanel v-if="!chapter.is_scorm_package">
<Draggable
@@ -88,7 +94,7 @@
>
<template #item="{ element: lesson }">
<div
class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9"
class="outline-lesson ps-8 py-2 pe-4 text-ink-gray-9"
:class="
isActiveLesson(lesson.number) ? 'bg-surface-gray-3' : ''
"
@@ -106,23 +112,23 @@
<div class="flex items-center text-sm leading-5 group">
<MonitorPlay
v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 stroke-1 mr-2"
class="h-4 w-4 stroke-1 me-2"
/>
<HelpCircle
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 stroke-1 mr-2"
class="h-4 w-4 stroke-1 me-2"
/>
<NotebookPen
v-else-if="lesson.icon === 'icon-assignment'"
class="h-4 w-4 stroke-1 mr-2"
class="h-4 w-4 stroke-1 me-2"
/>
<SquareCode
v-else-if="lesson.icon === 'icon-code'"
class="h-4 w-4 stroke-1 mr-2"
class="h-4 w-4 stroke-1 me-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-ink-gray-9 stroke-1 mr-2"
class="h-4 w-4 text-ink-gray-9 stroke-1 me-2"
/>
{{ lesson.title }}
<Trash2
@@ -130,18 +136,18 @@
@click.prevent="
trashLesson(lesson.name, chapter.name)
"
class="h-4 w-4 text-ink-red-3 ml-auto invisible group-hover:visible"
class="h-4 w-4 text-ink-red-3 ms-auto invisible group-hover:visible"
/>
<Check
v-if="lesson.is_complete"
class="h-4 w-4 text-green-700 ml-2"
class="h-4 w-4 text-green-700 ms-2"
/>
</div>
</router-link>
</div>
</template>
</Draggable>
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
<div v-if="allowEdit" class="flex mt-2 mb-4 ps-8">
<router-link
v-if="!chapter.is_scorm_package"
:to="{
@@ -401,6 +407,10 @@ const redirectToChapter = (chapter) => {
})
}
const isScormChapterComplete = (chapter) => {
return chapter.lessons?.length && chapter.lessons.every((l) => l.is_complete)
}
const isActiveLesson = (lessonNumber) => {
return (
route.params.chapterNumber == lessonNumber.split('-')[0] &&
+3 -3
View File
@@ -3,7 +3,7 @@
<Button
v-if="membership && !hasReviewed.data"
@click="openReviewModal()"
class="float-right"
class="float-end"
>
{{ __('Write a Review') }}
</Button>
@@ -28,14 +28,14 @@
params: { username: review.owner_details.username },
}"
>
<span class="text-lg font-medium mr-4 text-ink-gray-7">
<span class="text-lg font-medium me-4 text-ink-gray-7">
{{ review.owner_details.full_name }}
</span>
</router-link>
<span class="text-ink-gray-7">
{{ review.creation }}
</span>
<div class="flex mt-2 space-x-1">
<div class="flex mt-2 gap-x-1">
<Star
v-for="index in 5"
class="size-4 text-transparent rounded-sm"
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="flex h-screen w-screen">
<div class="h-full border-r bg-surface-menu-bar">
<div class="h-full border-e bg-surface-menu-bar">
<AppSidebar />
</div>
<div class="flex-1 flex flex-col h-full overflow-auto bg-surface-white">
@@ -6,7 +6,7 @@
<ChevronLeft class="w-5 h-5 stroke-1.5 text-ink-gray-7" />
</template>
</Button>
<span class="text-lg font-semibold ml-2 text-ink-gray-9">
<span class="text-lg font-semibold ms-2 text-ink-gray-9">
{{ topic.title }}
</span>
</div>
@@ -18,11 +18,11 @@
>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center text-ink-gray-5">
<UserAvatar :user="reply.user" class="mr-2" />
<UserAvatar :user="reply.user" class="me-2" />
<span>
{{ reply.user.full_name }}
</span>
<span class="text-sm ml-2">
<span class="text-sm ms-2">
{{ timeAgo(reply.creation) }}
</span>
</div>
+4 -4
View File
@@ -2,7 +2,7 @@
<div>
<Button
v-if="!singleThread && !readOnlyMode"
class="float-right"
class="float-end"
@click="openTopicModal()"
>
<template #prefix>
@@ -21,7 +21,7 @@
class="flex items-center cursor-pointer py-5 w-full"
:class="{ 'border-b': index + 1 != topics.data.length }"
>
<UserAvatar :user="topic.user" size="2xl" class="mr-4" />
<UserAvatar :user="topic.user" size="2xl" class="me-4" />
<div>
<div class="text-lg font-semibold mb-1 text-ink-gray-7">
{{ topic.title }}
@@ -30,7 +30,7 @@
<span>
{{ topic.user.full_name }}
</span>
<span class="text-sm ml-3">
<span class="text-sm ms-3">
{{ timeAgo(topic.creation) }}
</span>
</div>
@@ -51,7 +51,7 @@
v-else
class="flex flex-col items-center justify-center border-2 border-dashed mt-5 py-8 rounded-md"
>
<MessageSquareText class="w-7 h-7 text-ink-gray-4 stroke-1.5 mr-2" />
<MessageSquareText class="w-7 h-7 text-ink-gray-4 stroke-1.5 me-2" />
<div class="mt-2">
<div v-if="emptyStateTitle" class="font-medium mb-2">
{{ __(emptyStateTitle) }}
+1 -1
View File
@@ -34,7 +34,7 @@
<span class="inline-flex items-baseline">
<FeatherIcon
name="x"
class="ml-auto h-4 w-4 text-gray-700"
class="ms-auto h-4 w-4 text-gray-700"
@click="iosInstallMessage = false"
/>
</span>
+4 -4
View File
@@ -2,7 +2,7 @@
<div
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-3"
>
<div class="flex space-x-4 mb-4">
<div class="flex gap-x-4 mb-4">
<div class="flex flex-col space-y-2 flex-1 break-all">
<div class="text-lg font-semibold text-ink-gray-9">
{{ job.company_name }}
@@ -10,7 +10,7 @@
<span class="font-medium text-ink-gray-7 leading-5">
{{ job.job_title }}
</span>
<div class="flex items-center space-x-1 text-sm text-ink-gray-7">
<div class="flex items-center gap-x-1 text-sm text-ink-gray-7">
<MapPin class="size-3" />
<span>
{{ job.location }}{{ job.country ? `, ${job.country}` : '' }}
@@ -18,7 +18,7 @@
</div>
<div
v-if="job.applicants"
class="flex items-center space-x-1 text-sm text-ink-gray-7"
class="flex items-center gap-x-1 text-sm text-ink-gray-7"
>
<User class="size-3" />
<span>
@@ -29,7 +29,7 @@
</div>
<!-- <img :src="job.company_logo" alt="Company Logo" class="size-8 rounded-full object-contain bg-white" /> -->
</div>
<div class="space-x-2 mt-auto">
<div class="flex gap-x-2 items-center mt-auto">
<Badge>
{{ job.type }}
</Badge>
+4 -1
View File
@@ -57,7 +57,7 @@
>
</iframe>
</div>
<div v-else v-html="markdown.render(block)"></div>
<div v-else v-html="renderSafe(block)"></div>
</div>
<div v-if="quizId">
<Quiz :quiz="quizId" />
@@ -66,6 +66,7 @@
<script setup>
import Quiz from '@/components/QuizBlock.vue'
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
import { useScreenSize } from '@/utils/composables'
const screenSize = useScreenSize()
@@ -75,6 +76,8 @@ const markdown = new MarkdownIt({
linkify: true,
})
const renderSafe = (block) => DOMPurify.sanitize(markdown.render(block))
const props = defineProps({
content: {
type: String,
+2 -2
View File
@@ -1,7 +1,7 @@
<template>
<div class="space-y-5 text-ink-gray-9">
<div class="space-y-2">
<div class="flex items-center text-sm font-medium space-x-2">
<div class="flex items-center text-sm font-medium gap-x-2">
<span>
{{ __('What are Instructor Notes?') }}
</span>
@@ -17,7 +17,7 @@
<div class="space-y-2" v-for="(item, key) in contentMap" :key="key">
<div
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
class="flex items-center text-sm font-medium gap-x-2 cursor-pointer"
@click="openHelpDialog(key)"
>
<span>
+54 -61
View File
@@ -7,14 +7,14 @@
<div class="relative z-20">
<!-- Dropdown menu -->
<div
class="fixed bottom-16 right-2 w-[80%] rounded-md bg-surface-white text-base p-5 space-y-4 shadow-md"
class="fixed bottom-16 end-2 w-[80%] rounded-md bg-surface-white text-base p-5 space-y-4 shadow-md"
v-if="showMenu"
ref="menu"
>
<div
v-for="link in otherLinks"
:key="link.label"
class="flex items-center space-x-2 cursor-pointer"
class="flex items-center gap-x-2 cursor-pointer"
@click="handleClick(link)"
>
<component
@@ -28,7 +28,7 @@
<!-- Fixed menu -->
<div
v-if="sidebarSettings.data"
class="fixed bottom-0 left-0 w-full flex items-center justify-around border-t border-outline-gray-2 bg-surface-white standalone:pb-4 z-10"
class="fixed bottom-0 start-0 w-full flex items-center justify-around border-t border-outline-gray-2 bg-surface-white standalone:pb-4 z-10"
>
<button
v-for="tab in sidebarLinks"
@@ -57,7 +57,7 @@
import { getSidebarLinks } from '@/utils'
import { useRouter } from 'vue-router'
import { call } from 'frappe-ui'
import { watch, ref, onMounted } from 'vue'
import { ref, watch } from 'vue'
import { sessionStore } from '@/stores/session'
import { useSettings } from '@/stores/settings'
import { usersStore } from '@/stores/user'
@@ -68,26 +68,13 @@ let { isLoggedIn } = sessionStore()
const { sidebarSettings } = useSettings()
const router = useRouter()
let { userResource } = usersStore()
const sidebarLinks = ref(getSidebarLinks())
const sidebarLinks = ref([])
const otherLinks = ref([])
const showMenu = ref(false)
const menu = ref(null)
const isModerator = ref(false)
const isInstructor = ref(false)
onMounted(() => {
sidebarSettings.reload(
{},
{
onSuccess(data) {
destructureSidebarLinks()
filterLinksToShow(data)
addOtherLinks()
},
}
)
})
const handleOutsideClick = (e) => {
if (menu.value && !menu.value.contains(e.target)) {
showMenu.value = false
@@ -126,65 +113,57 @@ const filterLinksToShow = (data) => {
const addOtherLinks = () => {
if (user) {
otherLinks.value.push({
label: 'Notifications',
icon: 'Bell',
to: 'Notifications',
})
otherLinks.value.push({
label: 'Profile',
icon: 'UserRound',
})
otherLinks.value.push({
label: 'Log out',
icon: 'LogOut',
})
addLink('Notifications', 'Bell', 'Notifications')
addLink('Profile', 'UserRound')
addLink('Log out', 'LogOut')
} else {
otherLinks.value.push({
label: 'Log in',
icon: 'LogIn',
})
addLink('Log in', 'LogIn')
}
}
watch(userResource, () => {
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addPrograms()
if (isModerator.value || isInstructor.value) {
addProgrammingExercises()
addQuizzes()
addAssignments()
const addLink = (label, icon, to = '') => {
if (otherLinks.value.some((link) => link.label === label)) return
otherLinks.value.push({
label: label,
icon: icon,
to: to,
})
}
const updateSidebarLinks = () => {
sidebarLinks.value = getSidebarLinks(true)
destructureSidebarLinks()
sidebarSettings.reload(
{},
{
onSuccess: async (data) => {
filterLinksToShow(data)
await addPrograms()
if (isModerator.value || isInstructor.value) {
addQuizzes()
addAssignments()
addProgrammingExercises()
}
addOtherLinks()
},
}
}
})
)
}
const addQuizzes = () => {
otherLinks.value.push({
label: 'Quizzes',
icon: 'CircleHelp',
to: 'Quizzes',
})
addLink('Quizzes', 'CircleHelp', 'Quizzes')
}
const addAssignments = () => {
otherLinks.value.push({
label: 'Assignments',
icon: 'Pencil',
to: 'Assignments',
})
addLink('Assignments', 'Pencil', 'Assignments')
}
const addProgrammingExercises = () => {
otherLinks.value.push({
label: 'Programming Exercises',
icon: 'Code',
to: 'ProgrammingExercises',
})
addLink('Programming Exercises', 'Code', 'ProgrammingExercises')
}
const addPrograms = async () => {
if (sidebarLinks.value.some((link) => link.label === 'Programs')) return
let canAddProgram = await checkIfCanAddProgram()
if (!canAddProgram) return
let activeFor = ['Programs', 'ProgramDetail']
@@ -198,7 +177,21 @@ const addPrograms = async () => {
})
}
watch(
userResource,
async () => {
await userResource.promise
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
}
updateSidebarLinks()
},
{ immediate: true }
)
const checkIfCanAddProgram = async () => {
if (!userResource.data) return false
if (isModerator.value || isInstructor.value) {
return true
}
@@ -48,7 +48,7 @@
</div>
</div>
<div class="flex justify-end space-x-2 mt-5">
<div class="flex justify-end gap-x-2 mt-5">
<router-link
:to="{
name: 'AssignmentSubmissionList',
@@ -72,7 +72,7 @@
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { computed, reactive, watch } from 'vue'
import { escapeHTML, sanitizeHTML } from '@/utils'
import { sanitizeHTML } from '@/utils'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
@@ -133,7 +133,7 @@ watch(show, (newVal) => {
})
const validateFields = () => {
assignment.title = escapeHTML(assignment.title.trim())
assignment.title = sanitizeHTML(assignment.title.trim())
assignment.question = sanitizeHTML(assignment.question)
}
@@ -51,7 +51,7 @@
</FileUploader>
<div v-else class="">
<div class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<div class="border rounded-md p-2 me-2">
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
</div>
<div class="flex flex-col">
@@ -64,7 +64,7 @@
</div>
<X
@click="() => (chapter.scorm_package = null)"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ms-4"
/>
</div>
</div>
@@ -5,12 +5,12 @@
</template>
<template #body>
<div
class="absolute left-1/2 mt-3 w-96 max-w-lg -translate-x-1/2 transform rounded-lg bg-surface-white px-4 sm:px-0 lg:max-w-3xl"
class="absolute start-1/2 mt-3 w-96 max-w-lg -translate-x-1/2 transform rounded-lg bg-surface-white px-4 sm:px-0 lg:max-w-3xl"
>
<div
class="overflow-hidden rounded-lg p-3 shadow-2xl ring-1 ring-black ring-opacity-5"
>
<div class="flex items-center justify-center space-x-2">
<div class="flex items-center justify-center gap-x-2">
<TextInput
type="text"
placeholder="search by keyword"
@@ -10,11 +10,11 @@
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Edit Profile') }}
</div>
<div class="space-x-2">
<div class="flex items-center gap-x-2">
<Badge v-if="isDirty" theme="orange">
{{ __('Not Saved') }}
</Badge>
<div class="pb-5 float-right">
<div class="pb-5 float-end">
<Button variant="solid" @click="saveProfile()">
{{ __('Save') }}
</Button>
@@ -27,7 +27,7 @@
</div>
<div class="space-y-5">
<div v-for="row in slots.data" class="space-y-2">
<div class="flex items-center text-ink-gray-7 space-x-2">
<div class="flex items-center text-ink-gray-7 gap-x-2">
<Calendar class="size-3" />
<div class="text-ink-gray-9">
{{ dayjs(row.date).format('DD MMMM YYYY') }}
@@ -66,18 +66,12 @@
</Dialog>
</template>
<script setup>
import {
call,
createResource,
dayjs,
Dialog,
FormControl,
toast,
} from 'frappe-ui'
import { call, createResource, Dialog, FormControl, toast } from 'frappe-ui'
import { ref, watch, inject } from 'vue'
import { Calendar } from 'lucide-vue-next'
import { formatTime } from '@/utils/'
const dayjs = inject('$dayjs')
const user = inject('$user')
const show = defineModel()
const evaluations = defineModel('reloadEvals')
+8 -8
View File
@@ -14,7 +14,7 @@
<div class="flex flex-col space-y-4 text-sm text-ink-gray-8">
<Tooltip :text="__('Email ID')">
<div class="flex items-center space-x-2 w-fit">
<div class="flex items-center gap-x-2 w-fit">
<User class="h-4 w-4 stroke-1.5" />
<span>
{{ event.member }}
@@ -23,7 +23,7 @@
</Tooltip>
<Tooltip :text="__('Course')">
<div
class="flex space-x-2 w-fit cursor-pointer"
class="flex gap-x-2 w-fit cursor-pointer"
@click="openLink('course', event.course)"
>
<BookOpen class="h-4 w-4 stroke-1.5" />
@@ -34,7 +34,7 @@
</Tooltip>
<Tooltip v-if="event.batch_title" :text="__('Batch')">
<div
class="flex space-x-2 w-fit cursor-pointer"
class="flex gap-x-2 w-fit cursor-pointer"
@click="openLink('batch', event.batch_name)"
>
<Users class="h-4 w-4 stroke-1.5" />
@@ -44,7 +44,7 @@
</div>
</Tooltip>
<Tooltip :text="__('Date')">
<div class="flex items-center space-x-2 w-fit">
<div class="flex items-center gap-x-2 w-fit">
<Calendar class="h-4 w-4 stroke-1.5" />
<span>
{{ dayjs(event.date).format('DD MMM YYYY') }}
@@ -52,7 +52,7 @@
</div>
</Tooltip>
<Tooltip :text="__('Time')">
<div class="flex items-center space-x-2 w-fit">
<div class="flex items-center gap-x-2 w-fit">
<Clock class="h-4 w-4 stroke-1.5" />
<span>
{{ formatTime(event.start_time) }} -
@@ -61,7 +61,7 @@
</div>
</Tooltip>
</div>
<div class="flex items-center space-x-2 mt-auto">
<div class="flex items-center gap-x-2 mt-auto">
<Button
v-if="certificate.name"
@click="openCertificate(certificate)"
@@ -86,7 +86,7 @@
</Button>
</div>
</div>
<Tabs :tabs="tabs" as="div" v-model="tabIndex" class="border-l w-1/2">
<Tabs :tabs="tabs" as="div" v-model="tabIndex" class="border-s w-1/2">
<template #tab-panel="{ tab }">
<div
v-if="tab.label == 'Evaluation'"
@@ -247,7 +247,7 @@ const evaluationResource = createResource({
member: props.event.member,
course: props.event.course,
batch_name: props.event.batch_name,
date: props.event.date,
date_value: props.event.date,
start_time: props.event.start_time,
end_time: props.event.end_time,
status: evaluation.status,
@@ -2,7 +2,7 @@
<Dialog
v-model="show"
:options="{
size: '4xl',
size: '5xl',
}"
>
<template #body>
@@ -19,10 +19,17 @@
rowHeight: 'h-16',
selectable: false,
}"
class="border rounded-lg py-2 px-3"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
></ListHeader>
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none !px-0"
>
<ListHeaderItem :item="item" v-for="item in feedbackColumns">
<template #prefix="{ item }">
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" />
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow
:row="row"
@@ -41,7 +48,7 @@
class="flex"
:image="row['member_image']"
:label="item"
size="sm"
size="xl"
/>
</div>
</template>
@@ -63,9 +70,11 @@
<script setup lang="ts">
import {
Dialog,
ListView,
Avatar,
FeatherIcon,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
@@ -89,27 +98,43 @@ const feedbackColumns = computed(() => {
label: 'Member',
key: 'member_name',
width: '10rem',
align: 'left',
icon: 'user',
},
{
label: 'Feedback',
key: 'feedback',
width: '15rem',
align: 'left',
icon: 'message-square',
},
{
label: 'Content',
key: 'content',
width: '9rem',
width: '10rem',
align: 'center',
icon: 'book',
},
{
label: 'Instructors',
key: 'instructors',
width: '9rem',
width: '10rem',
align: 'center',
icon: 'users',
},
{
label: 'Value',
key: 'value',
width: '9rem',
width: '10rem',
align: 'center',
icon: 'dollar-sign',
},
]
})
</script>
<style>
.feedback-list > button > div {
padding: 0.2rem 0;
margin-bottom: 0.2rem;
}
</style>
@@ -51,7 +51,7 @@
</FileUploader>
</div>
<div v-else class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<div class="border rounded-md p-2 me-2">
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
</div>
<div class="flex flex-col">
@@ -31,7 +31,7 @@
@click="redirectToProfile(participant.member_username)"
class="grid grid-cols-2 items-center w-full text-base w-fit py-2"
>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<Avatar
:image="participant.member_image"
:label="participant.member_name"
@@ -47,7 +47,7 @@
</div>
</div>
<div class="grid grid-cols-3 gap-20 text-right">
<div class="grid grid-cols-3 gap-20 text-end">
<div>
{{ dayjs(participant.joined_at).format('HH:mm a') }}
</div>
+1 -1
View File
@@ -105,7 +105,7 @@
type="number"
/>
</div>
<div class="flex items-center justify-end space-x-2 mt-5">
<div class="flex items-center justify-end gap-x-2 mt-5">
<Button variant="solid" @click="submitQuestion()">
{{ __('Save') }}
</Button>
@@ -44,14 +44,14 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
class="h-4 w-4 stroke-1.5 ms-4"
/>
</template>
</ListHeaderItem>
@@ -18,7 +18,7 @@
<!-- <FormControl
v-model="searchText"
:placeholder="__('Search by Member')"
class="mt-2 mr-5 w-[25%]"
class="mt-2 me-5 w-[25%]"
/> -->
</div>
<div
@@ -50,7 +50,7 @@
}"
>
<div class="grid grid-cols-[70%,30%] items-center">
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<Avatar
:image="row.member_image"
:label="row.member_name"
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="text-base border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium text-ink-gray-9">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:me-2"
></span>
{{ __(title) }}
</div>
@@ -4,7 +4,7 @@
:style="{
display: top > 0 ? 'block' : 'none',
top: top + 'px',
left: left + 'px',
insetInlineStart: left + 'px',
}"
>
<div class="space-y-2 py-2">
@@ -14,7 +14,7 @@
<div class="">
<div
v-for="color in colors"
class="flex items-center space-x-2 px-3 py-2 cursor-pointer hover:bg-surface-gray-2"
class="flex items-center gap-x-2 px-3 py-2 cursor-pointer hover:bg-surface-gray-2"
@click="saveHighLight(color)"
>
<span
@@ -32,7 +32,7 @@
<div class="border-t">
<div
@click="addToNotes()"
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
class="flex items-center gap-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
>
<NotepadText class="size-3 stroke-1.5" />
<span>
@@ -42,7 +42,7 @@
<div
v-if="highlightExists()"
@click="deleteHighlight"
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
class="flex items-center gap-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
>
<Trash2 class="size-3 stroke-1.5" />
<span>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="text-ink-gray-5">
{{ __(title) }}
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<slot name="prefix" />
<div class="font-semibold text-ink-gray-9 text-2xl">
{{ value }}
+29 -23
View File
@@ -69,7 +69,7 @@
</ol>
</div>
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
<div v-if="quiz.data.duration" class="flex flex-col gap-x-1 my-4 px-2">
<div class="mb-2">
<span class="text-ink-gray-9"> {{ __('Time') }}: </span>
<span class="font-semibold text-ink-gray-9">
@@ -84,7 +84,7 @@
<div class="font-semibold text-lg text-ink-gray-9">
{{ quiz.data.title }}
</div>
<div class="flex items-center justify-center space-x-2 mt-4">
<div class="flex items-center justify-center gap-x-2 mt-4">
<Button
v-if="
!quiz.data.max_attempts ||
@@ -179,7 +179,7 @@
</div>
</div>
<span
class="ml-2 text-ink-gray-9"
class="ms-2 text-ink-gray-9"
v-html="questionDetails.data[`option_${index}`]"
>
</span>
@@ -202,12 +202,12 @@
<div v-if="showAnswers.length">
<Badge v-if="showAnswers[0]" :label="__('Correct')" theme="green">
<template #prefix>
<CheckCircle class="w-4 h-4 text-ink-green-2 mr-1" />
<CheckCircle class="w-4 h-4 text-ink-green-2 me-1" />
</template>
</Badge>
<Badge v-else theme="red" :label="__('Incorrect')">
<template #prefix>
<XCircle class="w-4 h-4 text-ink-red-3 mr-1" />
<XCircle class="w-4 h-4 text-ink-red-3 me-1" />
</template>
</Badge>
</div>
@@ -224,21 +224,14 @@
</div>
<div class="flex items-center justify-between mt-8">
<Checkbox
v-if="!quiz.data.show_answers"
:label="__('Mark for review')"
:model-value="reviewQuestions.includes(activeQuestion) ? 1 : 0"
@change="markForReview($event, activeQuestion)"
/>
<!-- <div class="text-sm text-ink-gray-5">
{{
__('Question {0} of {1}').format(
activeQuestion,
questions.length
)
}}
</div> -->
<div
v-if="!quiz.data.show_answers"
class="flex items-center space-x-2"
class="flex items-center gap-x-2"
>
<Button
@click="switchQuestion(activeQuestion - 1)"
@@ -257,11 +250,13 @@
'cursor-pointer': item !== '...',
'bg-surface-gray-4 border border-outline-gray-5 font-medium':
activeQuestion == item,
'bg-surface-gray-3 text-ink-gray-6':
activeQuestion != item && item !== '...',
'text-ink-gray-5': item === '...',
'bg-surface-blue-3 text-ink-white':
attemptedQuestions.includes(item) && activeQuestion != item,
'bg-surface-gray-3 text-ink-gray-6':
activeQuestion != item &&
item !== '...' &&
!attemptedQuestions.includes(item),
}"
@click="item !== '...' && switchQuestion(item)"
>
@@ -284,6 +279,7 @@
!showAnswers.length &&
questionDetails.data.type != 'Open Ended'
"
class="ms-auto"
@click="checkAnswer()"
>
<span>
@@ -295,12 +291,18 @@
activeQuestion != questions.length && quiz.data.show_answers
"
@click="nextQuestion()"
class="ms-auto"
>
<span>
{{ __('Next') }}
</span>
</Button>
<Button variant="solid" v-else @click="handleSubmitClick()">
<Button
variant="solid"
v-else
@click="handleSubmitClick()"
class="ms-auto"
>
<span>
{{ __('Submit') }}
</span>
@@ -312,10 +314,10 @@
<div class="font-semibold">
{{ __('Questions marked for review') }}
</div>
<div class="flex items-center space-x-2 mt-2">
<div class="flex items-center gap-x-2 mt-2">
<div
v-for="index in reviewQuestions"
@click="activeQuestion = index"
@click="switchQuestion(index)"
class="w-6 h-6 rounded-full flex items-center justify-center text-sm cursor-pointer bg-surface-gray-3"
>
{{ index }}
@@ -348,7 +350,7 @@
)
}}
</div>
<div class="space-x-2">
<div class="flex gap-x-2">
<Button
@click="resetQuiz()"
class="mt-2"
@@ -897,10 +899,14 @@ const markLessonProgress = () => {
}
const handleSubmitClick = () => {
if (attemptedQuestions.value.length) {
switchQuestion(activeQuestion.value)
if (!quiz.data.show_answers) {
if (attemptedQuestions.value.length) {
switchQuestion(activeQuestion.value)
}
showSubmissionConfirmation.value = true
} else {
submitQuiz()
}
showSubmissionConfirmation.value = true
}
const paginationWindow = computed(() => {
@@ -1,7 +1,7 @@
<template>
<div class="text-base">
<div class="flex items-center justify-between space-x-2 mb-5">
<div class="flex items-center space-x-2">
<div class="flex items-center justify-between gap-x-2 mb-5">
<div class="flex items-center gap-x-2">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-5 cursor-pointer"
@click="
@@ -34,7 +34,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
@@ -70,7 +70,7 @@
</div>
</template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<div class="pb-5 float-end">
<Button variant="solid" @click="saveBadge(close)">
{{ __('Save') }}
</Button>
+2 -2
View File
@@ -21,7 +21,7 @@
{{ __('New') }}
</Button>
</div>
<div v-if="badges.data?.length" class="overflow-y-scroll">
<div v-if="badges.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="badges.data"
@@ -32,7 +32,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns" :key="item.key">
<template #prefix="{ item }">
@@ -10,7 +10,7 @@
{{ __(description) }}
</div>
</div>
<div class="space-x-2">
<div class="flex items-center gap-x-2">
<Badge
v-if="isDirty"
:label="__('Not Saved')"
@@ -9,9 +9,9 @@
{{ __(description) }}
</div>
</div>
<div class="flex items-center space-x-5">
<div class="flex items-center gap-x-5">
<div
class="flex items-center space-x-1 text-ink-amber-3 border border-outline-amber-1 bg-surface-amber-1 rounded-lg px-2 py-1"
class="flex items-center gap-x-1 text-ink-amber-3 border border-outline-amber-1 bg-surface-amber-1 rounded-lg px-2 py-1"
v-if="saving"
>
<LoadingIndicator class="size-2" />
@@ -29,10 +29,7 @@
</div>
</div>
<div
v-if="showForm"
class="flex items-center justify-between my-4 space-x-2"
>
<div v-if="showForm" class="flex items-center justify-between my-4 gap-x-2">
<FormControl
ref="categoryInput"
v-model="category"
@@ -44,7 +41,7 @@
</Button>
</div>
<div class="overflow-y-scroll">
<div class="overflow-y-auto">
<div class="divide-y divide-outline-gray-modals space-y-2">
<div
v-for="(cat, index) in categories.data"
@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col text-base h-full">
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
<div class="flex items-center gap-x-2 mb-8 -ms-1.5">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
@@ -74,7 +74,7 @@
<CouponItems ref="couponItems" :data="data" :coupons="coupons" />
</div>
</div>
<div class="mt-auto space-x-2 ml-auto">
<div class="mt-auto flex gap-x-2 items-center ms-auto">
<Button variant="solid" @click="saveCoupon()">
{{ __('Save') }}
</Button>
@@ -1,7 +1,7 @@
<template>
<div>
<div class="relative overflow-x-auto border rounded-md">
<table class="w-full text-sm text-left text-ink-gray-5">
<table class="w-full text-sm text-start text-ink-gray-5">
<thead class="text-xs text-ink-gray-7 uppercase bg-surface-gray-2">
<tr>
<td scope="col" class="px-6 py-2">
@@ -17,7 +17,7 @@
</Button>
</div>
<div v-if="coupons.data?.length" class="overflow-y-scroll">
<div v-if="coupons.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="coupons.data"
@@ -31,7 +31,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
</ListHeader>
<ListRows>
@@ -9,7 +9,7 @@
{{ __(description) }}
</div> -->
</div>
<div class="flex items-center space-x-5">
<div class="flex items-center gap-x-5">
<Button @click="openTemplateForm('new')">
<template #prefix>
<Plus class="h-3 w-3 stroke-1.5" />
@@ -18,7 +18,7 @@
</Button>
</div>
</div>
<div v-if="emailTemplates.data?.length" class="overflow-y-scroll">
<div v-if="emailTemplates.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="emailTemplates.data"
@@ -31,14 +31,14 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
class="h-4 w-4 stroke-1.5 ms-4"
/>
</template>
</ListHeaderItem>
@@ -9,7 +9,7 @@
{{ __(description) }}
</div>
</div>
<div class="flex item-center space-x-2">
<div class="flex item-center gap-x-2">
<Dropdown
placement="right"
side="bottom"
@@ -39,7 +39,7 @@
<template #suffix>
<ChevronDown
:class="[
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
'w-4 h-4 stroke-1.5 ms-1 transform transition-transform',
open ? 'rotate-180' : '',
]"
/>
@@ -72,7 +72,7 @@
>
<div class="flex items-center justify-between group py-3">
<div
class="flex items-center space-x-3"
class="flex items-center gap-x-3"
@click="openProfile(evaluator.username)"
>
<Avatar
@@ -137,6 +137,7 @@ import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
import AddEvaluatorModal from '@/components/Modals/AddEvaluatorModal.vue'
const search = ref('')
const show = defineModel('show')
const showExistingUser = ref(false)
const showNewEvaluator = ref(false)
const router = useRouter()
@@ -173,6 +174,7 @@ watch(search, () => {
})
const openProfile = (username: string) => {
show.value = false
router.push({
name: 'Profile',
params: {
@@ -9,7 +9,7 @@
{{ __(description) }}
</div>
</div>
<div class="flex items-center space-x-5">
<div class="flex items-center gap-x-5">
<Button @click="openForm('new')">
<template #prefix>
<Plus class="h-3 w-3 stroke-1.5" />
@@ -18,7 +18,7 @@
</Button>
</div>
</div>
<div v-if="googleMeetAccounts.data?.length" class="overflow-y-scroll">
<div v-if="googleMeetAccounts.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="googleMeetAccounts.data"
@@ -31,7 +31,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
+4 -4
View File
@@ -9,7 +9,7 @@
{{ __(description) }}
</div>
</div>
<div class="flex item-center space-x-2">
<div class="flex item-center gap-x-2">
<Button @click="showNewMember = true">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
@@ -31,7 +31,7 @@
<Search class="size-4 stroke-1.5 text-ink-gray-5" />
</template>
</FormControl>
<div class="overflow-y-scroll max-h-[60vh]">
<div class="overflow-y-auto max-h-[60vh]">
<ul class="divide-y divide-outline-gray-modals">
<li
v-for="member in memberList"
@@ -39,7 +39,7 @@
>
<div
@click="openProfile(member.username)"
class="flex items-center space-x-3 col-span-2"
class="flex items-center gap-x-3 col-span-2"
>
<Avatar
:image="member.user_image"
@@ -58,7 +58,7 @@
</div>
</div>
<div
class="flex items-center text-ink-gray-9 space-x-1 bg-surface-gray-2 px-2 py-1.5 rounded-md"
class="flex items-center text-ink-gray-9 gap-x-1 bg-surface-gray-2 px-2 py-1.5 rounded-md"
v-if="member.role && member.role !== 'LMS Student'"
>
<Shield class="size-4 stroke-1.5" />
@@ -6,7 +6,7 @@
}"
>
<template #body-header>
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{
gatewayID === 'new'
? __('New Payment Gateway')
@@ -36,7 +36,7 @@
</div>
</template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<div class="pb-5 float-end">
<Button variant="solid" @click="saveSettings(close)">
{{ __('Save') }}
</Button>
@@ -17,7 +17,7 @@
</Button>
</div>
<div v-if="paymentGateways.data?.length" class="overflow-y-scroll">
<div v-if="paymentGateways.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="paymentGateways.data"
@@ -30,7 +30,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
@@ -10,7 +10,7 @@
{{ __(description) }}
</div>
</div>
<div class="space-x-2">
<div class="flex items-center gap-x-2">
<Badge
v-if="data.isDirty"
:label="__('Not Saved')"
@@ -6,7 +6,7 @@
</div>
<div
:class="{
'flex justify-between space-x-8 w-full': section.columns.length > 1,
'flex justify-between gap-x-8 w-full': section.columns.length > 1,
}"
>
<div
@@ -64,7 +64,7 @@
</template>
</FileUploader>
<div v-else>
<div class="flex items-center text-sm space-x-2">
<div class="flex items-center text-sm gap-x-2">
<div
class="flex items-center justify-center rounded border border-outline-gray-modals bg-surface-gray-2"
:class="field.size == 'lg' ? 'px-5 py-5' : 'px-20 py-8'"
@@ -91,7 +91,7 @@
</div>
<X
@click="data[field.name] = null"
class="border text-ink-gray-7 border-outline-gray-modals rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
class="border text-ink-gray-7 border-outline-gray-modals rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ms-4"
/>
</div>
</div>
@@ -31,7 +31,7 @@
<div
v-if="activeTab && data.doc"
:key="activeTab.label"
class="flex flex-1 flex-col p-8 bg-surface-modal overflow-x-auto"
class="flex flex-1 flex-col p-8 bg-surface-modal overflow-x-auto overflow-y-auto"
>
<component
v-if="activeTab.template"
@@ -43,6 +43,7 @@
? { sections: activeTab.sections }
: {}),
...(activeTab.label == 'Members' ||
activeTab.label == 'Evaluators' ||
activeTab.label == 'Transactions'
? { 'onUpdate:show': (val) => (show = val), show }
: {}),
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col h-full text-base">
<div class="flex items-center justify-between mb-10 -ml-1.5">
<div class="flex items-center space-x-2">
<div class="flex items-center justify-between mb-10 -ms-1.5">
<div class="flex items-center gap-x-2">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
@@ -10,7 +10,7 @@
{{ __('Transaction Details') }}
</div>
</div>
<div class="space-x-2">
<div class="flex items-center gap-x-2">
<Button
v-if="
transactionData?.payment_for_document_type &&
@@ -32,16 +32,14 @@
</div>
<div v-if="transactionData" class="overflow-y-auto">
<div class="grid grid-cols-3 gap-5">
<Switch
size="sm"
<FormControl
:label="__('Payment Received')"
:description="__('Mark the payment as received.')"
type="checkbox"
v-model="transactionData.payment_received"
/>
<Switch
size="sm"
<FormControl
:label="__('Payment For Certificate')"
:description="__('This payment is for a certificate.')"
type="checkbox"
v-model="transactionData.payment_for_certificate"
/>
<FormControl
@@ -87,7 +85,7 @@
/>
</div>
<div class="font-semibold mt-10">
<div class="font-semibold mt-10 text-ink-gray-9">
{{ __('Payment Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
@@ -111,7 +109,7 @@
</div>
<div v-if="transactionData.coupon">
<div class="font-semibold mt-10">
<div class="font-semibold mt-10 text-ink-gray-9">
{{ __('Coupon Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
@@ -142,7 +140,7 @@
</div>
</div>
<div class="font-semibold mt-10">
<div class="font-semibold mt-10 text-ink-gray-9">
{{ __('Billing Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
@@ -17,7 +17,7 @@
</Button>
</div>
<div class="flex items-center space-x-5 mb-4">
<div class="flex items-center gap-x-5 mb-4">
<FormControl
v-model="billingName"
:placeholder="__('Filter by Billing Name')"
@@ -27,35 +27,33 @@
doctype="User"
:placeholder="__('Filter by Member')"
/>
<Switch
size="sm"
:label="__('Payment Received')"
:description="__('Mark the payment as received.')"
<FormControl
v-model="paymentReceived"
type="checkbox"
:label="__('Payment Received')"
/>
<Switch
size="sm"
:label="__('Payment For Certificate')"
:description="__('This payment is for a certificate.')"
<FormControl
v-model="paymentForCertificate"
type="checkbox"
:label="__('Payment for Certificate')"
/>
</div>
<div v-if="transactions.data?.length" class="overflow-y-scroll">
<div v-if="transactions.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="transactions.data"
row-key="name"
:options="{
showTooltip: false,
selectable: false,
onRowClick: (row: { [key: string]: any }) => {
openForm(row)
},
}"
showTooltip: false,
selectable: false,
onRowClick: (row: { [key: string]: any }) => {
openForm(row)
},
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
@@ -9,7 +9,7 @@
{{ __(description || '') }}
</div>
</div>
<div class="flex items-center space-x-5">
<div class="flex items-center gap-x-5">
<Button @click="openForm('new')">
<template #prefix>
<Plus class="h-3 w-3 stroke-1.5" />
@@ -18,7 +18,7 @@
</Button>
</div>
</div>
<div v-if="zoomAccounts.data?.length" class="overflow-y-scroll">
<div v-if="zoomAccounts.data?.length" class="overflow-y-auto">
<ListView
:columns="columns"
:rows="zoomAccounts.data"
@@ -31,7 +31,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
+19 -14
View File
@@ -1,6 +1,6 @@
<template>
<div
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out border-r bg-surface-menu-bar"
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out border-e bg-surface-menu-bar overflow-x-hidden"
:class="sidebarStore.isSidebarCollapsed ? 'w-14' : 'w-56'"
>
<div
@@ -8,7 +8,7 @@
:class="sidebarStore.isSidebarCollapsed ? 'items-center' : ''"
>
<UserDropdown :isCollapsed="sidebarStore.isSidebarCollapsed" />
<div class="flex flex-col overflow-y-auto" v-if="sidebarSettings.data">
<div class="flex flex-col" v-if="sidebarSettings.data">
<div v-for="link in sidebarLinks" class="mx-2 my-2.5">
<div
v-if="!link.hideLabel"
@@ -31,8 +31,8 @@
class="mt-4"
>
<div
class="flex items-center justify-between pr-2 cursor-pointer"
:class="sidebarStore.isSidebarCollapsed ? 'pl-3' : 'pl-4'"
class="flex items-center justify-between pe-2 cursor-pointer"
:class="sidebarStore.isSidebarCollapsed ? 'ps-3' : 'ps-4'"
@click="toggleWebPages"
>
<div
@@ -42,10 +42,13 @@
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<ChevronRight
class="h-4 w-4 stroke-1.5 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': !sidebarStore.isWebpagesCollapsed }"
:class="{
'rotate-90': sidebarStore.isWebpagesCollapsed,
'rtl:rotate-180': !sidebarStore.isWebpagesCollapsed,
}"
/>
</span>
<span class="ml-2">
<span class="ms-2">
{{ __('More') }}
</span>
</div>
@@ -159,12 +162,8 @@
"
>
<div
class="flex items-center flex-1"
:class="
sidebarStore.isSidebarCollapsed
? 'flex-col space-y-3'
: 'flex-row space-x-3'
"
class="flex items-center flex-1 gap-3"
:class="sidebarStore.isSidebarCollapsed ? 'flex-col' : 'flex-row'"
>
<Tooltip v-if="readOnlyMode && sidebarStore.isSidebarCollapsed">
<CircleAlert
@@ -216,8 +215,11 @@
>
<CollapseSidebar
class="size-4 text-ink-gray-7 duration-300 stroke-1.5 ease-in-out cursor-pointer"
:class="{
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
:style="{
transform:
isRtl !== sidebarStore.isSidebarCollapsed
? 'rotateY(180deg)'
: '',
}"
@click="toggleSidebar()"
/>
@@ -225,6 +227,7 @@
</div>
</div>
<HelpModal
data-testid="onboarding-help-modal"
v-if="showOnboarding && showHelpModal"
v-model="showHelpModal"
v-model:articles="articles"
@@ -323,6 +326,7 @@ const router = useRouter()
let onboardingDetails
let isOnboardingStepsCompleted = false
const readOnlyMode = window.read_only_mode
const isRtl = document.documentElement.dir === 'rtl'
const iconProps = {
strokeWidth: 1.5,
width: 16,
@@ -670,6 +674,7 @@ watch(settingsStore.settings, () => {
const updateSidebarLinks = () => {
sidebarLinks.value = getSidebarLinks()
updateSidebarLinksVisibility()
updateUnreadCount()
}
const redirectToWebsite = () => {
@@ -25,18 +25,18 @@
class="flex-shrink-0 text-sm duration-300 ease-in-out"
:class="
isCollapsed
? 'ml-0 w-0 overflow-hidden opacity-0'
: 'ml-2 w-auto opacity-100'
? 'ms-0 w-0 overflow-hidden opacity-0'
: 'ms-2 w-auto opacity-100'
"
>
{{ __(link.label) }}
</span>
<span
v-if="link.count && !isCollapsed"
class="!ml-auto block text-xs text-ink-gray-5"
class="!ms-auto block text-xs text-ink-gray-5"
:class="
isCollapsed && link.count > 9
? 'absolute top-[2px] right-0 bg-surface-white'
? 'absolute top-[2px] end-0 bg-surface-white'
: ''
"
>
@@ -44,7 +44,7 @@
</span>
<div
v-if="showControls && !isCollapsed"
class="flex items-center space-x-2 !ml-auto block text-xs text-ink-gray-5 group-hover:visible invisible"
class="flex items-center gap-x-2 !ms-auto block text-xs text-ink-gray-5 group-hover:visible invisible"
>
<component
:is="icons['Edit']"
@@ -19,11 +19,11 @@
/>
<LMSLogo v-else class="w-8 h-8 rounded flex-shrink-0" />
<div
class="flex flex-1 flex-col text-left duration-300 ease-in-out"
class="flex flex-1 flex-col text-start duration-300 ease-in-out"
:class="
isCollapsed
? 'opacity-0 ml-0 w-0 overflow-hidden'
: 'opacity-100 ml-2 w-auto'
? 'opacity-0 ms-0 w-0 overflow-hidden'
: 'opacity-100 ms-2 w-auto'
"
>
<div class="text-base font-medium text-ink-gray-9 leading-none">
@@ -47,8 +47,8 @@
class="duration-300 ease-in-out"
:class="
isCollapsed
? 'opacity-0 ml-0 w-0 overflow-hidden'
: 'opacity-100 ml-2 w-auto'
? 'opacity-0 ms-0 w-0 overflow-hidden'
: 'opacity-100 ms-2 w-auto'
"
>
<ChevronDown class="h-4 w-4 text-ink-gray-7" />
@@ -68,6 +68,7 @@ import { sessionStore } from '@/stores/session'
import { call, Dropdown, toast } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { convertToTitleCase } from '@/utils'
import { applyTheme, toggleTheme, theme } from '@/utils/theme'
import { usersStore } from '@/stores/user'
import { useSettings } from '@/stores/settings'
import { markRaw, watch, ref, onMounted, computed } from 'vue'
@@ -94,7 +95,6 @@ let { userResource } = usersStore()
const settingsStore = useSettings()
let { isLoggedIn } = sessionStore()
const showSettingsModal = ref(false)
const theme = ref('light')
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
const $dialog = createDialog
@@ -106,9 +106,8 @@ const props = defineProps({
})
onMounted(() => {
theme.value = localStorage.getItem('theme') || 'light'
if (['light', 'dark'].includes(theme.value)) {
document.documentElement.setAttribute('data-theme', theme.value)
applyTheme(theme.value)
}
})
@@ -119,13 +118,6 @@ watch(
}
)
const toggleTheme = () => {
const currentTheme = document.documentElement.getAttribute('data-theme')
theme.value = currentTheme === 'dark' ? 'light' : 'dark'
document.documentElement.setAttribute('data-theme', theme.value)
localStorage.setItem('theme', theme.value)
}
const userDropdownOptions = computed(() => {
return [
{
+2 -2
View File
@@ -7,11 +7,11 @@
{{ tags }}
<div
v-for="tag in tags?.split(', ')"
class="flex items-center bg-surface-gray-2 p-2 rounded-md mr-2"
class="flex items-center bg-surface-gray-2 p-2 rounded-md me-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
class="stroke-1.5 w-3 h-3 ms-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
@@ -5,12 +5,12 @@
</template>
<template #body>
<div
class="absolute left-1/2 mt-3 max-w-sm -translate-x-1/2 transform rounded-lg bg-surface-white px-4 sm:px-0 lg:max-w-3xl"
class="absolute start-1/2 mt-3 max-w-sm -translate-x-1/2 transform rounded-lg bg-surface-white px-4 sm:px-0 lg:max-w-3xl"
>
<div
class="overflow-hidden rounded-lg p-3 shadow-2xl ring-1 ring-black ring-opacity-5"
>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<div class="flex-1">
<TextInput
type="text"
@@ -66,25 +66,25 @@
</div>
<div class="flex items-center mb-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
<span class="ms-2">
{{ dayjs(evl.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center mb-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
<span class="ms-2">
{{ formatTime(evl.start_time) }}
</span>
</div>
<div class="flex items-center">
<GraduationCap class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
<span class="ms-2">
{{ evl.evaluator_name }}
</span>
</div>
<div
v-if="evl.google_meet_link"
class="flex items-center justify-between space-x-2 mt-4"
class="flex items-center justify-between gap-x-2 mt-4"
>
<Button @click="openEvalCall(evl)" class="w-full">
<template #prefix>
+5 -5
View File
@@ -8,7 +8,7 @@
)
}}
<div v-for="(quiz, index) in quizzes" class="pl-3 mt-1">
<div v-for="(quiz, index) in quizzes" class="ps-3 mt-1">
<span>
{{ index + 1 }}. <span class="font-semibold"> {{ quiz.quiz }} </span>
</span>
@@ -36,7 +36,7 @@
@click="playVideo"
>
<div
class="rounded-full p-4 pl-4.5"
class="rounded-full p-4 ps-4.5"
style="
background: radial-gradient(
circle,
@@ -49,7 +49,7 @@
</div>
</div>
<div
class="flex items-center space-x-2 py-2 px-1 text-ink-white bg-gradient-to-b from-transparent to-black/75 absolute bottom-0 left-0 right-0 mx-auto rounded-md"
class="flex items-center gap-x-2 py-2 px-1 text-ink-white bg-gradient-to-b from-transparent to-black/75 absolute bottom-0 start-0 end-0 mx-auto rounded-md"
:class="{
'invisible group-hover:visible': playing,
}"
@@ -76,7 +76,7 @@
class="duration-slider h-1"
/>
<!-- QUIZ MARKERS -->
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<div class="absolute top-0 start-0 w-full h-full pointer-events-none">
<div
v-for="(quiz, index) in quizzes"
:key="index"
@@ -336,7 +336,7 @@ const toggleFullscreen = () => {
const getQuizMarkerStyle = (time) => {
const percentage = ((time - 5) / Math.ceil(duration.value)) * 100
return {
left: `${percentage}%`,
insetInlineStart: `${percentage}%`,
}
}
@@ -26,7 +26,7 @@
rowKey="name"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in submissionColumns" />
</ListHeader>
+114 -33
View File
@@ -20,18 +20,17 @@
</Button>
</header>
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
<div class="flex items-center justify-between mb-5">
<div v-if="assignmentCount" class="text-lg font-semibold text-ink-gray-9">
{{ __('{0} Assignments').format(assignmentCount) }}
<div class="py-5">
<div
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 justify-between mb-5 mx-5"
>
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('{0} Assignments').format(assignments.data?.length) }}
</div>
<div
v-if="assignments.data?.length || assignmentCount > 0"
class="grid grid-cols-2 gap-5"
>
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="titleFilter"
:placeholder="__('Search by title')"
:placeholder="__('Search by Title')"
/>
<FormControl
v-model="typeFilter"
@@ -48,23 +47,77 @@
row-key="name"
:options="{
showTooltip: false,
selectable: false,
selectable: true,
onRowClick: (row) => {
if (readOnlyMode) return
assignmentID = row.name
showAssignmentForm = true
},
}"
class="h-[71vh] lg:h-[79vh] px-5"
>
<ListHeader
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
>
<ListHeaderItem :item="item" v-for="item in assignmentColumns">
<template #prefix="{ item }">
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" />
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow
v-for="row in assignments.data"
:row="row"
class="hover:bg-surface-gray-2"
>
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div v-if="column.key == 'show_answers'">
<FormControl
type="checkbox"
v-model="row[column.key]"
:disabled="true"
/>
</div>
<div
v-else-if="column.key == 'modified'"
class="text-sm text-ink-gray-5"
>
{{ row[column.key] }}
</div>
<div v-else>
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner class="bottom-50">
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="deleteAssignment(selections, unselectAll)"
>
<FeatherIcon name="trash-2" class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
<EmptyState v-else type="Assignments" />
<div
v-if="assignments.data && assignments.hasNextPage"
class="flex justify-center my-5"
>
<Button @click="assignments.next()">
<div v-else class="h-[53vh]">
<EmptyState type="Assignments" />
</div>
<div class="flex items-center justify-end gap-x-3 pt-3 border-t px-5">
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
{{ __('Load More') }}
</Button>
<div v-if="assignments.hasNextPage" class="h-8 border-s"></div>
<div class="text-ink-gray-5">
{{ assignments.data?.length }} {{ __('of') }}
{{ totalAssignments.data }}
</div>
</div>
</div>
<AssignmentForm
@@ -79,8 +132,17 @@ import {
Button,
call,
createListResource,
createResource,
FormControl,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
ListSelectBanner,
FeatherIcon,
toast,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue'
@@ -96,7 +158,6 @@ const titleFilter = ref('')
const typeFilter = ref('')
const showAssignmentForm = ref(false)
const assignmentID = ref('new')
const assignmentCount = ref(0)
const { brand } = sessionStore()
const router = useRouter()
const route = useRoute()
@@ -110,7 +171,6 @@ onMounted(() => {
assignmentID.value = 'new'
showAssignmentForm.value = true
}
getAssignmentCount()
titleFilter.value = router.currentRoute.value.query.title
typeFilter.value = router.currentRoute.value.query.type
})
@@ -123,6 +183,10 @@ watch([titleFilter, typeFilter], () => {
},
})
reloadAssignments()
totalAssignments.update({
filters: assignmentFilter.value,
})
totalAssignments.reload()
})
const reloadAssignments = () => {
@@ -137,7 +201,7 @@ const assignmentFilter = computed(() => {
if (titleFilter.value) {
filters.title = ['like', `%${titleFilter.value}%`]
}
if (typeFilter.value) {
if (typeFilter.value && typeFilter.value.trim() !== '') {
filters.type = typeFilter.value
}
return filters
@@ -145,51 +209,60 @@ const assignmentFilter = computed(() => {
const assignments = createListResource({
doctype: 'LMS Assignment',
fields: ['name', 'title', 'type', 'creation', 'question', 'course'],
fields: ['name', 'title', 'type', 'modified', 'question', 'course'],
orderBy: 'modified desc',
cache: ['assignments'],
transform(data) {
return data.map((row) => {
return {
...row,
creation: dayjs(row.creation).fromNow(),
modified: dayjs(row.modified).format('DD MMM YYYY'),
}
})
},
})
const totalAssignments = createResource({
url: 'frappe.client.get_count',
params: {
doctype: 'LMS Assignment',
filters: assignmentFilter.value,
},
auto: true,
cache: ['assignments_count', user.data?.name],
onError(err) {
toast.error(err.messages?.[0] || err)
console.error(err)
},
})
const assignmentColumns = computed(() => {
return [
{
label: __('Title'),
key: 'title',
width: 2,
width: 1,
icon: 'file-text',
},
{
label: __('Type'),
key: 'type',
width: 1,
align: 'left',
icon: 'tag',
},
{
label: __('Created'),
key: 'creation',
label: __('Updated On'),
key: 'modified',
width: 1,
align: 'right',
icon: 'clock',
},
]
})
const getAssignmentCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Assignment',
}).then((data) => {
assignmentCount.value = data
})
}
const assignmentTypes = computed(() => {
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
let types = [' ', 'Document', 'Image', 'PDF', 'URL', 'Text']
return types.map((type) => {
return {
label: __(type),
@@ -198,6 +271,14 @@ const assignmentTypes = computed(() => {
})
})
const deleteAssignment = (selections, unselectAll) => {
Array.from(selections).forEach(async (assignmentName) => {
await assignments.delete.submit(assignmentName)
})
unselectAll()
toast.success(__('Assignments deleted successfully'))
}
const breadcrumbs = computed(() => [
{
label: __('Assignments'),
+1 -1
View File
@@ -4,7 +4,7 @@
class="sticky top-0 z-10 border-b flex items-center justify-between bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
<div v-if="tabIndex == 5 && isAdmin" class="flex items-center space-x-2">
<div v-if="tabIndex == 5 && isAdmin" class="flex items-center gap-x-2">
<Badge v-if="childRef?.isDirty" theme="orange">
{{ __('Not Saved') }}
</Badge>
+2 -22
View File
@@ -275,7 +275,7 @@
</div>
</div>
</div>
<div class="border-l min-w-0">
<div class="border-s min-w-0">
<div class="border-b p-4">
<BatchCourses :batch="batch" />
</div>
@@ -321,15 +321,12 @@ import {
} from 'frappe-ui'
import {
createLMSCategory,
escapeHTML,
getMetaInfo,
openSettings,
sanitizeHTML,
updateMetaInfo,
} from '@/utils'
import { useRouter } from 'vue-router'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { sessionStore } from '@/stores/session'
import { useTelemetry } from 'frappe-ui/frappe'
import Uploader from '@/components/Controls/Uploader.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
@@ -340,8 +337,6 @@ import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue'
const router = useRouter()
const user = inject('$user')
const { brand } = sessionStore()
const { updateOnboardingStep } = useOnboarding('learning')
const instructors = ref([])
const app = getCurrentInstance()
const { capture } = useTelemetry()
@@ -462,22 +457,7 @@ const formatTime = (timeStr) => {
return `${hours}:${minutes}`
}
const validateFields = () => {
batchDetail.doc.description = sanitizeHTML(batchDetail.doc.description)
batchDetail.doc.batch_details = sanitizeHTML(batchDetail.doc.batch_details)
Object.keys(batchDetail.doc).forEach((key) => {
if (
!['description', 'batch_details'].includes(key) &&
typeof batchDetail.doc[key] === 'string'
) {
batchDetail.doc[key] = escapeHTML(batchDetail.doc[key])
}
})
}
const submitBatch = () => {
validateFields()
updateBatch()
}
+1 -1
View File
@@ -10,7 +10,7 @@
</div>
<div class="flex avatar-group overlap">
<div
class="h-6 mr-1"
class="h-6 me-1"
:class="{
'avatar-group overlap': batch.data.instructors.length > 1,
}"
+11 -10
View File
@@ -34,7 +34,7 @@
<template #suffix>
<ChevronDown
:class="[
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
'w-4 h-4 stroke-1.5 ms-1 transform transition-transform',
open ? 'rotate-180' : '',
]"
/>
@@ -51,7 +51,7 @@
{{ __('All Batches') }}
</div>
<div
class="flex flex-col space-y-3 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
class="flex flex-col space-y-3 lg:space-y-0 lg:flex-row lg:items-center lg:gap-x-4"
>
<TabButtons
v-if="user.data"
@@ -78,13 +78,14 @@
</div>
</div>
<Switch
size="sm"
v-model="certification"
:label="__('Certification')"
:description="__('Only show batches that offer a certificate.')"
@change="updateBatches()"
/>
<Tooltip :text="__('Only show batches that offer a certificate')">
<FormControl
type="checkbox"
v-model="certification"
:label="__('Certification')"
@change="updateBatches()"
/>
</Tooltip>
</div>
</div>
<div
@@ -123,7 +124,7 @@ import {
Dropdown,
FormControl,
Select,
Switch,
Tooltip,
TabButtons,
usePageMeta,
} from 'frappe-ui'
@@ -26,11 +26,11 @@
<div class="grid grid-cols-1 lg:grid-cols-[3fr_2fr] gap-5 items-start">
<div class="border rounded-lg py-3 px-4 order-2 lg:order-1">
<div class="flex items-center justify-between space-x-2 mb-3">
<div class="flex items-center justify-between gap-x-2 mb-3">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Students') }}
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<FormControl
v-model="searchFilter"
:placeholder="__('Search by name')"
@@ -62,7 +62,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-white border-b rounded-none p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-white border-b rounded-none p-2"
>
<ListHeaderItem
:item="item"
@@ -91,7 +91,7 @@
<!-- <ProgressBar
v-else-if="column.key == 'progress'"
:progress="Math.ceil(row[column.key])"
class="!mx-0 !mr-4"
class="!mx-0 !me-4"
/> -->
</template>
<div v-if="column.key == 'creation'">
@@ -174,7 +174,6 @@ import {
AxisChart,
createResource,
createListResource,
dayjs,
FormControl,
ListView,
ListHeader,
@@ -185,7 +184,8 @@ import {
Avatar,
Button,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import type dayjsType from 'dayjs'
import { formatAmount } from '@/utils'
import { Plus } from 'lucide-vue-next'
import BatchFeedback from '@/pages/Batches/components/BatchFeedback.vue'
@@ -193,6 +193,7 @@ import BatchStudentProgress from '@/pages/Batches/components/BatchStudentProgres
import NumberChartGraph from '@/components/NumberChartGraph.vue'
import StudentModal from '@/components/Modals/StudentModal.vue'
const dayjs = inject<typeof dayjsType>('$dayjs')!
const searchFilter = ref<string | null>(null)
const showEnrollmentModal = ref<boolean>(false)
const showProgressModal = ref<boolean>(false)
@@ -9,7 +9,7 @@
<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 text-ink-gray-7">
<div class="ms-2 text-ink-gray-7">
{{ comm.sender_full_name }}
</div>
</div>
@@ -24,7 +24,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in getAssessmentColumns()">
</ListHeaderItem>
@@ -39,8 +39,8 @@
class="text-sm text-ink-gray-7"
/>
<div class="flex items-center text-sm text-ink-gray-7">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-ink-gray-7" />
<span>
<Clock class="h-4 w-4 stroke-1.5 me-2 text-ink-gray-7" />
<span dir="ltr">
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
</span>
</div>
@@ -48,7 +48,7 @@
v-if="batch.timezone"
class="flex items-center text-sm text-ink-gray-7"
>
<Globe class="h-4 w-4 stroke-1.5 mr-2 text-ink-gray-5" />
<Globe class="h-4 w-4 stroke-1.5 me-2 text-ink-gray-5" />
<span>
{{ batch.timezone }}
</span>
@@ -59,7 +59,7 @@
class="flex avatar-group overlap mt-4"
>
<div
class="h-6 mr-1"
class="h-6 me-1"
:class="{ 'avatar-group overlap': batch.instructors.length > 1 }"
>
<UserAvatar
@@ -108,6 +108,6 @@ const props = defineProps({
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
margin-inline-start: calc(-8px);
}
</style>
@@ -27,7 +27,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in getCoursesColumns()">
</ListHeaderItem>
@@ -35,7 +35,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
>
</ListHeader>
<ListRows>
@@ -65,7 +65,7 @@
<Assessments :batch="batch.data.name" />
</div>
</div>
<div class="border-l h-[88vh] divide-y">
<div class="border-s h-[88vh] divide-y">
<div v-if="batch.data?.evaluation" class="p-4 mb-5">
<UpcomingEvaluations
:batch="batch.data.name"
@@ -183,9 +183,3 @@ const isAdmin = computed(() => {
return user.data?.is_moderator || user.data?.is_evaluator
})
</script>
<style>
.feedback-list > button > div {
align-items: start;
padding: 0.15rem 0;
}
</style>
@@ -1,100 +1,108 @@
<template>
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
<Badge
v-if="batch.data.seat_count && batch.data.seats_left > 0"
variant="subtle"
theme="green"
size="md"
:class="
batch.data.amount || batch.data.courses.length
? 'float-right'
: 'w-fit mb-4'
"
:label="
batch.data.seats_left +
' ' +
(batch.data.seats_left > 1 ? __('Seats Left') : __('Seat Left'))
"
<div v-if="batch.data" class="border-2 rounded-md lg:w-72">
<video
v-if="batch.data.video_link"
:src="batch.data.video_link"
controls
class="rounded-t-md w-full"
/>
<Badge
v-else-if="batch.data.seat_count && batch.data.seats_left <= 0"
variant="subtle"
theme="red"
size="md"
class="float-right"
:label="__('Sold Out')"
/>
<div
v-if="batch.data.amount"
class="text-lg font-semibold mb-5 text-ink-gray-9"
>
{{ formatNumberIntoCurrency(batch.data.amount, batch.data.currency) }}
</div>
<div
v-if="batch.data.courses.length"
class="flex items-center mb-3 text-ink-gray-7"
>
<BookOpen class="h-4 w-4 stroke-1.5 mr-2" />
<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 text-ink-gray-7">
<Clock class="h-4 w-4 stroke-1.5 mr-2" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div v-if="batch.data.timezone" class="flex items-center text-ink-gray-7">
<Globe class="h-4 w-4 stroke-1.5 mr-2" />
<span>
{{ batch.data.timezone }}
</span>
</div>
<div class="p-5">
<Badge
v-if="batch.data.seat_count && batch.data.seats_left > 0"
variant="subtle"
theme="green"
size="md"
:class="
batch.data.amount || batch.data.courses.length
? 'float-end'
: 'w-fit mb-4'
"
:label="
batch.data.seats_left +
' ' +
(batch.data.seats_left > 1 ? __('Seats Left') : __('Seat Left'))
"
/>
<Badge
v-else-if="batch.data.seat_count && batch.data.seats_left <= 0"
variant="subtle"
theme="red"
size="md"
class="float-end"
:label="__('Sold Out')"
/>
<div
v-if="batch.data.amount"
class="text-lg font-semibold mb-5 text-ink-gray-9"
>
{{ formatNumberIntoCurrency(batch.data.amount, batch.data.currency) }}
</div>
<div
v-if="batch.data.courses.length"
class="flex items-center mb-3 text-ink-gray-7"
>
<BookOpen class="h-4 w-4 stroke-1.5 me-2" />
<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 text-ink-gray-7">
<Clock class="h-4 w-4 stroke-1.5 me-2" />
<span dir="ltr">
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div v-if="batch.data.timezone" class="flex items-center text-ink-gray-7">
<Globe class="h-4 w-4 stroke-1.5 me-2" />
<span>
{{ batch.data.timezone }}
</span>
</div>
<div v-if="!readOnlyMode && !canAccessBatch">
<router-link
:to="{
name: 'Billing',
params: {
type: 'batch',
name: batch.data.name,
},
}"
v-if="
batch.data.paid_batch &&
batch.data.seats_left > 0 &&
batch.data.accept_enrollments
"
>
<Button class="w-full mt-4" variant="solid">
<div v-if="!readOnlyMode && !canAccessBatch">
<router-link
:to="{
name: 'Billing',
params: {
type: 'batch',
name: batch.data.name,
},
}"
v-if="
batch.data.paid_batch &&
batch.data.seats_left > 0 &&
batch.data.accept_enrollments
"
>
<Button class="w-full mt-4" variant="solid">
<template #prefix>
<CreditCard class="size-4 stroke-1.5" />
</template>
<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 &&
batch.data.accept_enrollments
"
@click="enrollInBatch()"
>
<template #prefix>
<CreditCard class="size-4 stroke-1.5" />
<GraduationCap class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Register Now') }}
</span>
{{ __('Enroll Now') }}
</Button>
</router-link>
<Button
variant="solid"
class="w-full mt-2"
v-else-if="
batch.data.allow_self_enrollment &&
batch.data.seats_left &&
batch.data.accept_enrollments
"
@click="enrollInBatch()"
>
<template #prefix>
<GraduationCap class="size-4 stroke-1.5" />
</template>
{{ __('Enroll Now') }}
</Button>
</div>
</div>
</div>
</template>
@@ -3,14 +3,21 @@
v-model="show"
:options="{
size: 'xl',
title: studentDetails.data?.full_name || __('Student Details'),
}"
>
<template #body>
<div v-if="studentDetails.data" class="p-5 space-y-10 text-sm">
<div class="flex items-center space-x-2">
<div
v-if="studentDetails.loading && !studentDetails.data"
class="flex items-center justify-center py-12"
>
<LoadingIndicator class="size-4" />
</div>
<div v-else-if="studentDetails.data" class="p-5 space-y-10 text-sm">
<div class="flex items-center gap-x-2">
<Avatar :image="studentDetails.data.user_image" size="3xl" />
<div class="space-y-1">
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<div class="text-xl font-semibold text-ink-gray-9">
{{ studentDetails.data.full_name }}
</div>
@@ -46,7 +53,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
>
</ListHeader>
<ListRows v-for="row in studentDetails.data.assessments">
@@ -88,7 +95,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
class="mb-2 grid items-center gap-x-4 rounded-none rounded-t bg-surface-gray-2 p-2"
>
</ListHeader>
<ListRows v-for="row in studentDetails.data.courses">
@@ -103,12 +110,12 @@
<ProgressBar
v-if="column.key == 'progress'"
:progress="Math.ceil(row[column.key])"
class="!mx-0 !mr-4 max-w-32"
class="!mx-0 !me-4 max-w-32"
/>
</template>
<div
v-if="column.key == 'progress'"
class="text-xs !ml-0 !mr-3 w-5"
class="text-xs !ms-0 !me-3 w-5"
>
{{ Math.ceil(row[column.key]) }}%
</div>
@@ -136,6 +143,7 @@ import {
ListRows,
ListRow,
ListRowItem,
LoadingIndicator,
} from 'frappe-ui'
import { useRouter } from 'vue-router'
import ProgressBar from '@/components/ProgressBar.vue'
@@ -2,7 +2,7 @@
<div class="p-5">
<div
v-if="isAdmin() && !hasProviderAccount()"
class="flex lg:items-center space-x-2 mb-5 bg-surface-amber-1 px-3 py-2 rounded-lg text-ink-amber-3"
class="flex lg:items-center gap-x-2 mb-5 bg-surface-amber-1 px-3 py-2 rounded-lg text-ink-amber-3"
>
<AlertCircle class="size-7 md:size-4 stroke-1.5" />
<span class="leading-5">
@@ -50,13 +50,13 @@
{{ cls.description }}
</div>
<div class="mt-auto space-y-3">
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span>
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ dayjs(getClassStart(cls)).format('hh:mm A') }} -
@@ -65,7 +65,7 @@
</div>
<div
v-if="canAccessClass(cls) && cls.join_url"
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
class="flex items-center gap-x-2 text-ink-gray-9 mt-auto"
>
<a
v-if="user.data?.is_moderator || user.data?.is_evaluator"
@@ -91,7 +91,7 @@
:text="__('This class has ended')"
placement="right"
>
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
<div class="flex items-center gap-x-2 text-ink-amber-3 w-fit">
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('Ended') }}
@@ -102,7 +102,7 @@
</div>
</template>
<template #actions="{ close }">
<div class="text-right">
<div class="text-end">
<Button variant="solid" @click="saveBatch(close)">
{{ __('Save') }}
</Button>
@@ -120,7 +120,7 @@ import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { sanitizeHTML, escapeHTML, createLMSCategory } from '@/utils'
import { sanitizeHTML, createLMSCategory } from '@/utils'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
@@ -179,16 +179,9 @@ const onInstructorCreated = (user: any) => {
}
const validateFields = () => {
batch.value.description = sanitizeHTML(batch.value.description)
batch.value.batch_details = sanitizeHTML(batch.value.batch_details)
Object.keys(batch.value).forEach((key) => {
if (
key != 'description' &&
key != 'batch_details' &&
typeof batch.value[key as keyof Batch] === 'string'
) {
batch.value[key as keyof Batch] = escapeHTML(
if (typeof batch.value[key as keyof Batch] === 'string') {
batch.value[key as keyof Batch] = sanitizeHTML(
batch.value[key as keyof Batch] as string
)
}
+14 -19
View File
@@ -63,7 +63,7 @@
<span class="text-ink-gray-5 uppercase text-xs">
{{ __('Enter a Coupon Code') }}:
</span>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<FormControl
v-model="appliedCoupon"
:disabled="orderSummary.data.discount_amount > 0"
@@ -103,7 +103,7 @@
</p>
</div>
<div class="flex-1 lg:mr-10">
<div class="flex-1 lg:me-10">
<div class="mb-5">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Address') }}
@@ -199,8 +199,15 @@
}}
</div>
</div>
<Button variant="solid" size="md" @click="generatePaymentLink()">
{{ __('Proceed to Payment') }}
<Button
variant="solid"
size="md"
class="ms-auto"
@click="generatePaymentLink()"
>
{{
isZeroAmount ? __('Enroll for Free') : __('Proceed to Payment')
}}
</Button>
</div>
</div>
@@ -326,16 +333,10 @@ const paymentLink = createResource({
let data = {
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
docname: props.name,
title: orderSummary.data.title,
amount: orderSummary.data.original_amount,
discount_amount: orderSummary.data.discount_amount || 0,
gst_amount: orderSummary.data.gst_applied || 0,
currency: orderSummary.data.currency,
address: billingDetails,
redirect_to: redirectTo.value,
payment_for_certificate: props.type == 'certificate',
coupon_code: appliedCoupon.value,
coupon: orderSummary.data.coupon,
country: billingDetails.country,
}
return data
},
@@ -458,14 +459,8 @@ const changeCurrency = (country) => {
orderSummary.reload()
}
const redirectTo = computed(() => {
if (props.type == 'course') {
return getLmsRoute(`courses/${props.name}`)
} else if (props.type == 'batch') {
return getLmsRoute(`batches/${props.name}`)
} else if (props.type == 'certificate') {
return getLmsRoute(`courses/${props.name}/certification`)
}
const isZeroAmount = computed(() => {
return orderSummary.data && parseFloat(orderSummary.data.total_amount) <= 0
})
watch(billingDetails, () => {
+68 -68
View File
@@ -12,15 +12,15 @@
</Button>
</router-link>
</header>
<div class="mx-auto w-full max-w-4xl pt-6 pb-10">
<div class="flex flex-col md:flex-row justify-between mb-8 px-3">
<div class="text-xl font-semibold text-ink-gray-9 mb-4 md:mb-0">
<div class="mx-auto w-full">
<div class="flex flex-col md:flex-row justify-between mb-5 px-5 pt-5">
<div class="text-lg font-semibold text-ink-gray-9 mb-4 md:mb-0">
{{ memberCount }} {{ __('Certified Members') }}
</div>
<div
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 md:space-x-4"
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 md:gap-x-4"
>
<div class="flex items-center space-x-4">
<div class="flex items-center gap-x-4">
<FormControl
v-model="nameFilter"
:placeholder="__('Search by Name')"
@@ -40,100 +40,100 @@
/>
</div>
</div>
<div class="flex items-center space-x-4">
<Switch
size="sm"
<div class="flex items-center gap-x-4">
<FormControl
v-model="openToWork"
:label="__('Open to Work')"
type="checkbox"
@change="updateParticipants()"
/>
<Switch
size="sm"
<FormControl
v-model="hiring"
:label="__('Hiring')"
type="checkbox"
@change="updateParticipants()"
/>
</div>
</div>
</div>
<div v-if="participants.data?.length" class="">
<template v-for="(participant, index) in participants.data">
<router-link
:to="{
name: 'ProfileAbout',
params: {
username: participant.username,
},
}"
<div
v-if="participants.data?.length"
class="h-[63vh] lg:h-[77vh] overflow-y-auto mb-5 px-5"
>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
<div
v-for="participant in participants.data"
class="flex flex-col border hover:border-outline-gray-3 rounded-lg p-3 text-ink-gray-9 cursor-pointer"
@click="
router.push({
name: 'ProfileAbout',
params: { username: participant.username },
})
"
>
<div class="rounded-md hover:bg-surface-gray-2 px-3">
<div
class="flex items-center w-full space-x-3 py-2"
:class="{
'border-b': index < participants.data.length - 1,
}"
>
<UserAvatar :user="participant" size="2xl" />
<div class="flex flex-col md:flex-row w-full">
<div class="flex-1">
<div class="text-base font-medium text-ink-gray-8">
{{ participant.full_name }}
</div>
<div
v-if="participant.headline"
class="mt-1.5 text-base text-ink-gray-5"
>
{{ participant.headline }}
</div>
</div>
<div
class="flex items-center space-x-3 md:space-x-24 text-sm md:text-base mt-1.5"
>
<div class="text-ink-gray-5">
{{ participant.certificate_count }}
{{
participant.certificate_count > 1
? __('certificates')
: __('certificate')
}}
</div>
<span class="text-ink-gray-4 md:hidden">·</span>
<div class="text-ink-gray-5">
{{ dayjs(participant.issue_date).format('DD MMM YYYY') }}
</div>
</div>
<div class="flex items-center gap-x-4">
<UserAvatar :user="participant" size="2xl" />
<div class="flex flex-col">
<div class="font-semibold line-clamp-1">
{{ participant.full_name }}
</div>
<div class="text-sm leading-5 line-clamp-1 mb-4">
{{
participant.headline ||
'Joined ' + dayjs(participant.creation).fromNow()
}}
</div>
</div>
</div>
</router-link>
</template>
<div class="mt-auto space-y-2 text-ink-gray-7">
<div class="flex items-center gap-x-1">
<GraduationCap class="h-4 w-4 stroke-1.5 me-1" />
<span>
{{ participant.certificate_count }}
{{
participant.certificate_count > 1
? __('certificates')
: __('certificate')
}}
</span>
</div>
<div class="flex items-center gap-x-1">
<Calendar class="h-4 w-4 stroke-1.5 me-1" />
<span>{{
dayjs(participant.issue_date).format('DD MMM YYYY')
}}</span>
</div>
</div>
</div>
</div>
</div>
<EmptyState v-else type="Certified Members" />
<div
v-if="!participants.list.loading && participants.hasNextPage"
class="flex justify-center mt-5"
>
<Button @click="participants.next()">
<div v-else class="h-[40vh] lg:h-[53vh] px-5">
<EmptyState type="Certified Members" />
</div>
<div class="flex items-center justify-end gap-x-3 border-t pt-3 px-5">
<Button v-if="participants.hasNextPage" @click="participants.next()">
{{ __('Load More') }}
</Button>
<div v-if="participants.hasNextPage" class="h-8 border-s"></div>
<div class="text-ink-gray-5">
{{ participants.data?.length }} {{ __('of') }}
{{ memberCount }}
</div>
</div>
</div>
</template>
<script setup>
import {
Avatar,
Breadcrumbs,
Button,
call,
createListResource,
FormControl,
Select,
Switch,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue'
import { GraduationCap } from 'lucide-vue-next'
import { GraduationCap, Calendar } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
@@ -163,8 +163,8 @@ const participants = createListResource({
doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certified_participants',
start: 0,
pageLength: 40,
cache: ['certified_participants'],
pageLength: 100,
})
const getMemberCount = () => {
@@ -25,7 +25,7 @@
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Students') }}
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center gap-x-2">
<FormControl
v-model="searchFilter"
:placeholder="__('Search by name')"
@@ -53,7 +53,7 @@
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-white border-b rounded-none p-2"
class="mb-2 grid items-center gap-x-4 rounded bg-surface-white border-b rounded-none p-2"
>
<ListHeaderItem
:item="item"
@@ -91,7 +91,7 @@
<ProgressBar
v-else-if="column.key == 'progress'"
:progress="Math.ceil(row[column.key])"
class="!mx-0 !mr-4"
class="!mx-0 !me-4"
/>
</template>
<div v-if="column.key == 'creation'">
@@ -153,12 +153,12 @@
}"
></div>
<Tooltip :text="row.name.split('(')[1].replace(')', '')">
<div class="ml-2">
<div class="ms-2">
{{ row.name.split('(')[0] }}
</div>
</Tooltip>
<Tooltip :text="row.value">
<div class="ml-auto">
<div class="ms-auto">
{{
Math.round((row.value / course.data?.enrollments) * 100)
}}%
@@ -221,7 +221,7 @@
class="flex justify-between text-sm py-2 my-1 text-ink-gray-9"
>
<div class="">
<span class="mr-3 text-xs">
<span class="me-3 text-xs">
{{ progress.chapter_idx }}.{{ progress.idx }}
</span>
<span>
@@ -264,7 +264,6 @@ import {
Button,
createListResource,
createResource,
dayjs,
Dropdown,
ECharts,
FormControl,
@@ -277,7 +276,8 @@ import {
Select,
Tooltip,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import type dayjsType from 'dayjs'
import { Plus, Star } from 'lucide-vue-next'
import { formatAmount } from '@/utils'
import colors from '@/utils/frappe-ui-colors.json'
@@ -290,6 +290,7 @@ const props = defineProps<{
course: any
}>()
const dayjs = inject<typeof dayjsType>('$dayjs')!
const showEnrollmentModal = ref(false)
const searchFilter = ref<string | null>(null)
const showProgressModal = ref(false)
+89 -8
View File
@@ -4,15 +4,19 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div v-if="tabIndex == 2" class="flex items-center space-x-2">
<div v-if="tabIndex == 2 && isAdmin" class="flex items-center gap-x-2">
<Badge v-if="childRef?.isDirty" theme="orange">
{{ __('Not Saved') }}
</Badge>
<Button @click="childRef.trashCourse()">
<template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" />
<Dropdown :options="courseMenu" side="left">
<template v-slot="{ open }">
<Button>
<template #icon>
<Ellipsis class="w-4 h-4 stroke-1.5" />
</template>
</Button>
</template>
</Button>
</Dropdown>
<Button variant="solid" @click="childRef.submitCourse()">
{{ __('Save') }}
</Button>
@@ -31,16 +35,26 @@
<script setup>
import {
Badge,
Button,
createResource,
Breadcrumbs,
Button,
call,
createResource,
Dropdown,
Tabs,
toast,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, markRaw, onMounted, ref, watch } from 'vue'
import { sessionStore } from '@/stores/session'
import { useRouter, useRoute } from 'vue-router'
import { List, Settings2, Trash2, TrendingUp } from 'lucide-vue-next'
import {
Download,
Ellipsis,
List,
Settings2,
Trash2,
TrendingUp,
} from 'lucide-vue-next'
import CourseOverview from '@/pages/Courses/CourseOverview.vue'
import CourseDashboard from '@/pages/Courses/CourseDashboard.vue'
import CourseForm from '@/pages/Courses/CourseForm.vue'
@@ -139,6 +153,73 @@ const isAdmin = computed(() => {
return user.data?.is_moderator || isInstructor()
})
const exportCourse = async () => {
try {
const response = await fetch(
'/api/method/lms.lms.api.export_course_as_zip?course_name=' +
course.data.name,
{
method: 'GET',
credentials: 'include',
}
)
if (!response.ok) {
const errorText = await response.text()
console.error('Error response:', errorText)
throw new Error('Download failed')
}
const blob = await response.blob()
const disposition = response.headers.get('Content-Disposition')
let filename = 'course.zip'
if (disposition && disposition.includes('filename=')) {
filename = disposition.split('filename=')[1].replace(/"/g, '')
}
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
} catch (err) {
console.error(err)
toast.error('Export failed')
}
}
const download_course_zip = (data) => {
const a = document.createElement('a')
a.href = data.export_url
a.download = data.name
a.click()
}
const courseMenu = computed(() => {
let options = [
{
label: __('Export'),
onClick() {
exportCourse()
},
icon: Download,
},
{
label: __('Delete'),
onClick() {
childRef.value.trashCourse()
},
icon: Trash2,
},
]
return options
})
const breadcrumbs = computed(() => {
let crumbs = [{ label: __('Courses'), route: { name: 'Courses' } }]
crumbs.push({
@@ -42,7 +42,7 @@
</div>
</template>
<template #actions="{ close }">
<div class="text-right">
<div class="text-end">
<Button variant="solid" @click="enrollStudent(close)">
{{ __('Enroll') }}
</Button>

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