diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 254ef1e7..bfe2b8b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,15 @@ on: push: branches: - main + - develop + - main-hotfix pull_request: {} jobs: tests: + name: Server Tests runs-on: ubuntu-latest + strategy: + fail-fast: false services: redis-cache: image: redis:alpine @@ -30,13 +35,13 @@ jobs: steps: - uses: actions/checkout@v2 - name: setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.14' - name: setup node - uses: actions/setup-node@v2 + uses: actions/setup-node@v6 with: - node-version: '18' + node-version: '24' check-latest: true - name: setup cache for bench uses: actions/cache@v4 @@ -69,6 +74,9 @@ jobs: - name: setup requirements working-directory: /home/runner/frappe-bench run: bench setup requirements --dev + - name: block endpoints + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local set-config block_endpoints 1 - name: allow tests working-directory: /home/runner/frappe-bench run: bench --site frappe.local set-config allow_tests true @@ -77,4 +85,27 @@ jobs: run: bench --site frappe.local build - name: run tests working-directory: /home/runner/frappe-bench - run: bench --site frappe.local run-tests --app lms \ No newline at end of file + run: bench --site frappe.local run-tests --app lms --coverage + - name: Upload coverage data + uses: actions/upload-artifact@v4 + with: + path: /home/runner/frappe-bench/sites/coverage.xml + + coverage: + name: Coverage Wrap Up + needs: tests + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Download artifacts + uses: actions/download-artifact@v4 + + - name: Upload coverage data + uses: codecov/codecov-action@v5 + with: + name: Server + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + verbose: true \ No newline at end of file diff --git a/.github/workflows/generate-pot-file.yml b/.github/workflows/generate-pot-file.yml index 4c5d81e1..eea3e93a 100644 --- a/.github/workflows/generate-pot-file.yml +++ b/.github/workflows/generate-pot-file.yml @@ -22,9 +22,14 @@ jobs: ref: ${{ matrix.branch }} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 - name: Run script to update POT file run: | diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index b0ecb717..06b29eb7 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -16,9 +16,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 200 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 check-latest: true - name: Check commit titles @@ -35,9 +35,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.14' - name: Cache pip uses: actions/cache@v4 diff --git a/.github/workflows/make_release_pr.yml b/.github/workflows/make_release_pr.yml index 4c0890c1..9a4528b9 100644 --- a/.github/workflows/make_release_pr.yml +++ b/.github/workflows/make_release_pr.yml @@ -18,9 +18,9 @@ jobs: owner: frappe repo: lms title: |- - "chore: merge 'develop' into 'main'" + "chore: merge 'main-hotfix' into 'main'" body: "Automated weekly release" base: main - head: develop + head: main-hotfix env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml index cc7b1c89..623fd9f8 100644 --- a/.github/workflows/on_release.yml +++ b/.github/workflows/on_release.yml @@ -15,9 +15,9 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 - name: Setup dependencies run: | npm install @semantic-release/git @semantic-release/exec --no-save diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 6ff03b93..111c5918 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -4,7 +4,10 @@ on: pull_request: workflow_dispatch: push: - branches: [ main ] + branches: + - main + - develop + - main-hotfix permissions: # Do not change this as GITHUB_TOKEN is being used by roulette @@ -36,9 +39,9 @@ jobs: uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.14' - name: Check for valid Python & Merge Conflicts run: | @@ -48,9 +51,9 @@ jobs: exit 1 fi - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v6 with: - node-version: 18 + node-version: 24 check-latest: true - name: Add to Hosts diff --git a/.gitignore b/.gitignore index 95242ce7..c532a8dc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ node_modules package-lock.json lms/public/frontend lms/www/lms.html -frappe-ui \ No newline at end of file +lms/www/_lms.html +frappe-ui +frappe-semgrep-rules \ No newline at end of file diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 00000000..098cfd0a --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,30 @@ +pull_request_rules: + - name: backport to develop + conditions: + - label="backport develop" + actions: + backport: + branches: + - develop + assignees: + - "{{ author }}" + + - name: backport to main-hotfix + conditions: + - label="backport main-hotfix" + actions: + backport: + branches: + - main-hotfix + assignees: + - "{{ author }}" + + - name: backport to main + conditions: + - label="backport main" + actions: + backport: + branches: + - main + assignees: + - "{{ author }}" diff --git a/.releaserc b/.releaserc index 4f5437e0..6c6d1954 100644 --- a/.releaserc +++ b/.releaserc @@ -1,5 +1,5 @@ { - "branches": ["develop"], + "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", { "preset": "angular" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..4f6dfa59 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "**/test_helper.py" \ No newline at end of file diff --git a/cypress/e2e/batch_creation.cy.js b/cypress/e2e/batch_creation.cy.js index 9ec55c2c..f0fe6ed1 100644 --- a/cypress/e2e/batch_creation.cy.js +++ b/cypress/e2e/batch_creation.cy.js @@ -27,6 +27,10 @@ describe("Batch Creation", () => { cy.get("input[placeholder='Jane']").type(randomName); cy.get("button").contains("Add").click(); + // Open Settings + cy.get("span").contains("Learning").click(); + cy.get("span").contains("Settings").click(); + // Add evaluator cy.get("[data-dismissable-layer]") .find("span") @@ -48,26 +52,23 @@ describe("Batch Creation", () => { // Create a batch cy.get("button").contains("Create").click(); + cy.get("span").contains("New Batch").click(); cy.wait(500); - cy.url().should("include", "/batches/new/edit"); cy.get("label").contains("Title").type("Test Batch"); - cy.get("label").contains("Start Date").type("2030-10-01"); cy.get("label").contains("End Date").type("2030-10-31"); cy.get("label").contains("Start Time").type("10:00"); cy.get("label").contains("End Time").type("11:00"); cy.get("label").contains("Timezone").type("IST"); cy.get("label").contains("Seat Count").type("10"); - cy.get("label").contains("Published").click(); cy.get("label") - .contains("Short Description") + .contains("Description") .type("Test Batch Short Description to test the UI"); cy.get("div[contenteditable=true").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." ); - /* Instructor */ cy.get("label") .contains("Instructors") @@ -85,13 +86,14 @@ describe("Batch Creation", () => { cy.get("[id^=headlessui-combobox-option-").first().click(); }); }); - + cy.button("Save").click(); + cy.get("label").contains("Published").click(); cy.button("Save").click(); cy.wait(1000); let batchName; cy.url().then((url) => { console.log(url); - batchName = url.split("/").pop(); + batchName = url.split("/").pop().split("#")[0]; cy.wrap(batchName).as("batchName"); }); cy.wait(500); @@ -110,7 +112,7 @@ describe("Batch Creation", () => { .click(); cy.get("@batchName").then((batchName) => { - cy.get(`a[href='/lms/batches/details/${batchName}'`).within(() => { + cy.get(`a[href='/lms/batches/${batchName}'`).within(() => { cy.get("div").contains("Test Batch").should("be.visible"); cy.get("div") .contains("Test Batch Short Description to test the UI") @@ -123,14 +125,11 @@ describe("Batch Creation", () => { .should("be.visible"); cy.get("span").contains("IST").should("be.visible"); cy.get("a").contains("Evaluator").should("be.visible"); - cy.get("div") - .contains("10") - .should("be.visible") - .get("span") - .contains("Seats Left") - .should("be.visible"); + cy.contains("div:visible", "10 Seats Left").should( + "be.visible" + ); }); - cy.get(`a[href='/lms/batches/details/${batchName}'`).click(); + cy.get(`a[href='/lms/batches/${batchName}'`).click(); }); cy.get("div").contains("Test Batch").should("be.visible"); @@ -152,17 +151,22 @@ describe("Batch Creation", () => { "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." ) .should("be.visible"); - cy.get("button:visible").contains("Manage Batch").click(); + cy.get("button:visible").contains("Dashboard").click(); /* Add student to batch */ - cy.get("button").contains("Add").click(); - cy.get('div[role="dialog"]').first().find("button").eq(1).click(); - cy.get("input[id^='headlessui-combobox-input-v-']").type(randomEmail); + cy.get("button").contains("Enroll").click(); + cy.get('div[role="dialog"]') + .first() + .find("div[label='Student']") + .find("div") + .first() + .click(); + cy.get("input[placeholder='Search']").type(randomEmail); cy.get("div").contains(randomEmail).click(); cy.get("button").contains("Submit").click(); // Verify Seat Count - cy.get("span").contains("Details").click(); + cy.get("button:visible").contains("Overview").click(); cy.contains("div:visible", "9 Seats Left").should("be.visible"); }); }); diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index a0d14591..b5f5ae73 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -9,8 +9,8 @@ describe("Course Creation", () => { // Create a course cy.get("button").contains("Create").click(); + cy.get("span").contains("New Course").click(); cy.wait(500); - cy.url().should("include", "/courses/new/edit"); cy.get("label").contains("Title").type("Test Course"); cy.get("label") @@ -34,27 +34,13 @@ describe("Course Creation", () => { }); }); - cy.get("label") - .contains("Preview Video") - .type("https://www.youtube.com/embed/-LPmw2Znl2c"); - cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}"); - cy.get("label") - .contains("Category") - .parent() - .within(() => { - cy.get("button").click(); - }); - cy.get("[id^=headlessui-combobox-option-") - .should("be.visible") - .first() - .click(); - /* Instructor */ cy.get("label") .contains("Instructors") .parent() .within(() => { cy.get("input").click().type("frappe"); + cy.wait(500); cy.get("input") .invoke("attr", "aria-controls") .as("instructor_list_id"); @@ -67,13 +53,29 @@ describe("Course Creation", () => { }); }); + cy.button("Save").last().click(); + + // Edit Course Details + cy.wait(500); + cy.get("label") + .contains("Preview Video") + .type("https://www.youtube.com/embed/-LPmw2Znl2c"); + cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}"); + cy.get("label") + .contains("Category") + .parent() + .within(() => { + cy.get("button").click(); + }); + cy.get("div").contains("Business").click(); + cy.get("label").contains("Published").click(); cy.get("label").contains("Published On").type("2021-01-01"); cy.button("Save").click(); // Add Chapter cy.wait(1000); - cy.button("Add Chapter").click(); + cy.button("Add").click(); cy.wait(1000); cy.get("[data-dismissable-layer]") diff --git a/frappe-ui b/frappe-ui index f1bde9bc..78025c67 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit f1bde9bcb271af47e9f5de190a18dff8604f5312 +Subproject commit 78025c679490705b8df9fc0162985f5f3c808568 diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts index 9d240079..3568bad8 100644 --- a/frontend/auto-imports.d.ts +++ b/frontend/auto-imports.d.ts @@ -6,5 +6,7 @@ // biome-ignore lint: disable export {} declare global { - + const LucideGithub: typeof import('~icons/lucide/github').default + const LucideLinkedin: typeof import('~icons/lucide/linkedin').default + const LucideTwitter: typeof import('~icons/lucide/twitter').default } diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 1f5ac102..42b89f05 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -8,13 +8,10 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { - Annoucements: typeof import('./src/components/Annoucements.vue')['default'] - AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default'] - Apps: typeof import('./src/components/Apps.vue')['default'] - AppSidebar: typeof import('./src/components/AppSidebar.vue')['default'] + Apps: typeof import('./src/components/Sidebar/Apps.vue')['default'] + AppSidebar: typeof import('./src/components/Sidebar/AppSidebar.vue')['default'] AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default'] AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default'] - Assessments: typeof import('./src/components/Assessments.vue')['default'] Assignment: typeof import('./src/components/Assignment.vue')['default'] AssignmentForm: typeof import('./src/components/Modals/AssignmentForm.vue')['default'] AudioBlock: typeof import('./src/components/AudioBlock.vue')['default'] @@ -23,16 +20,8 @@ declare module 'vue' { BadgeAssignments: typeof import('./src/components/Settings/BadgeAssignments.vue')['default'] BadgeForm: typeof import('./src/components/Settings/BadgeForm.vue')['default'] Badges: typeof import('./src/components/Settings/Badges.vue')['default'] - BatchCard: typeof import('./src/components/BatchCard.vue')['default'] BatchCourseModal: typeof import('./src/components/Modals/BatchCourseModal.vue')['default'] - BatchCourses: typeof import('./src/components/BatchCourses.vue')['default'] - BatchDashboard: typeof import('./src/components/BatchDashboard.vue')['default'] - BatchFeedback: typeof import('./src/components/BatchFeedback.vue')['default'] - BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default'] - BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default'] - BatchStudents: typeof import('./src/components/BatchStudents.vue')['default'] BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default'] - BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default'] Categories: typeof import('./src/components/Settings/Categories.vue')['default'] CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default'] ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default'] @@ -41,12 +30,18 @@ declare module 'vue' { CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default'] CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default'] ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default'] + CommandPalette: typeof import('./src/components/CommandPalette/CommandPalette.vue')['default'] + CommandPaletteGroup: typeof import('./src/components/CommandPalette/CommandPaletteGroup.vue')['default'] + Configuration: typeof import('./src/components/Sidebar/Configuration.vue')['default'] ContactUsEmail: typeof import('./src/components/ContactUsEmail.vue')['default'] + CouponDetails: typeof import('./src/components/Settings/Coupons/CouponDetails.vue')['default'] + CouponItems: typeof import('./src/components/Settings/Coupons/CouponItems.vue')['default'] + CouponList: typeof import('./src/components/Settings/Coupons/CouponList.vue')['default'] + Coupons: typeof import('./src/components/Settings/Coupons/Coupons.vue')['default'] CourseCard: typeof import('./src/components/CourseCard.vue')['default'] CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default'] CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default'] CourseOutline: typeof import('./src/components/CourseOutline.vue')['default'] - CourseProgressSummary: typeof import('./src/components/Modals/CourseProgressSummary.vue')['default'] CourseReviews: typeof import('./src/components/CourseReviews.vue')['default'] CreateOutline: typeof import('./src/components/CreateOutline.vue')['default'] DateRange: typeof import('./src/components/Common/DateRange.vue')['default'] @@ -75,7 +70,6 @@ declare module 'vue' { LessonContent: typeof import('./src/components/LessonContent.vue')['default'] LessonHelp: typeof import('./src/components/LessonHelp.vue')['default'] Link: typeof import('./src/components/Controls/Link.vue')['default'] - LiveClass: typeof import('./src/components/LiveClass.vue')['default'] LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default'] LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default'] LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default'] @@ -86,6 +80,7 @@ declare module 'vue' { NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default'] Notes: typeof import('./src/components/Notes/Notes.vue')['default'] NotPermitted: typeof import('./src/components/NotPermitted.vue')['default'] + NumberChartGraph: typeof import('./src/components/NumberChartGraph.vue')['default'] PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] PaymentGatewayDetails: typeof import('./src/components/Settings/PaymentGatewayDetails.vue')['default'] PaymentGateways: typeof import('./src/components/Settings/PaymentGateways.vue')['default'] @@ -103,18 +98,19 @@ declare module 'vue' { SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default'] SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default'] Settings: typeof import('./src/components/Settings/Settings.vue')['default'] - SidebarLink: typeof import('./src/components/SidebarLink.vue')['default'] + SidebarLink: typeof import('./src/components/Sidebar/SidebarLink.vue')['default'] StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default'] StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default'] Tags: typeof import('./src/components/Tags.vue')['default'] - TransactionDetails: typeof import('./src/components/Settings/TransactionDetails.vue')['default'] - Transactions: typeof import('./src/components/Settings/Transactions.vue')['default'] + TransactionDetails: typeof import('./src/components/Settings/Transactions/TransactionDetails.vue')['default'] + TransactionList: typeof import('./src/components/Settings/Transactions/TransactionList.vue')['default'] + Transactions: typeof import('./src/components/Settings/Transactions/Transactions.vue')['default'] UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default'] UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default'] Uploader: typeof import('./src/components/Controls/Uploader.vue')['default'] UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default'] UserAvatar: typeof import('./src/components/UserAvatar.vue')['default'] - UserDropdown: typeof import('./src/components/UserDropdown.vue')['default'] + UserDropdown: typeof import('./src/components/Sidebar/UserDropdown.vue')['default'] VideoBlock: typeof import('./src/components/VideoBlock.vue')['default'] VideoStatistics: typeof import('./src/components/Modals/VideoStatistics.vue')['default'] ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default'] diff --git a/frontend/package.json b/frontend/package.json index 83805f7c..fc19b151 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,55 +6,57 @@ "scripts": { "dev": "vite", "serve": "vite preview", - "build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry", - "copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html" + "build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry && yarn copy-colors-json", + "copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/_lms.html", + "copy-colors-json": "cp node_modules/frappe-ui/tailwind/colors.json src/utils/frappe-ui-colors.json" }, "dependencies": { - "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-python": "^6.2.1", - "@editorjs/checklist": "^1.6.0", - "@editorjs/code": "^2.9.0", - "@editorjs/editorjs": "^2.29.0", - "@editorjs/embed": "^2.7.0", - "@editorjs/header": "^2.8.1", - "@editorjs/inline-code": "^1.5.0", - "@editorjs/nested-list": "^1.4.2", - "@editorjs/paragraph": "^2.11.3", - "@editorjs/simple-image": "^1.6.0", - "@editorjs/table": "^2.4.2", - "@vueuse/router": "^12.7.0", - "ace-builds": "^1.36.2", - "apexcharts": "^4.3.0", - "chart.js": "^4.4.1", - "codemirror": "^6.0.1", - "dayjs": "^1.11.6", - "dompurify": "^3.2.6", - "feather-icons": "^4.28.0", - "frappe-ui": "^0.1.201", - "highlight.js": "^11.11.1", - "lucide-vue-next": "^0.383.0", - "markdown-it": "^14.0.0", - "pinia": "^2.0.33", - "plyr": "^3.7.8", - "socket.io-client": "^4.7.2", - "tailwindcss": "3.4.15", - "thememirror": "^2.0.1", - "typescript": "^5.7.2", - "vue": "^3.4.23", - "vue-chartjs": "^5.3.0", - "vue-codemirror": "^6.1.1", - "vue-draggable-next": "^2.2.1", - "vue-router": "^4.0.12", - "vue3-apexcharts": "^1.8.0", + "@codemirror/lang-html": "6.4.9", + "@codemirror/lang-javascript": "6.2.4", + "@codemirror/lang-json": "6.0.1", + "@codemirror/lang-python": "6.2.1", + "@editorjs/checklist": "1.6.0", + "@editorjs/code": "2.9.0", + "@editorjs/editorjs": "2.29.0", + "@editorjs/embed": "2.7.0", + "@editorjs/header": "2.8.1", + "@editorjs/inline-code": "1.5.0", + "@editorjs/nested-list": "1.4.2", + "@editorjs/paragraph": "2.11.3", + "@editorjs/simple-image": "1.6.0", + "@editorjs/table": "2.4.2", + "@vueuse/core": "^14.1.0", + "ace-builds": "1.36.2", + "apexcharts": "4.3.0", + "chart.js": "4.4.1", + "codemirror": "6.0.1", + "dayjs": "1.11.10", + "dompurify": "3.2.6", + "feather-icons": "4.28.0", + "frappe-ui": "^0.1.261", + "highlight.js": "11.11.1", + "lucide-vue-next": "0.383.0", + "markdown-it": "14.0.0", + "pinia": "2.0.33", + "plyr": "3.7.8", + "socket.io-client": "4.7.2", + "thememirror": "2.0.1", + "typescript": "5.7.2", + "vue": "^3.5.27", + "vue-chartjs": "5.3.0", + "vue-codemirror": "6.1.1", + "vue-draggable-next": "2.2.1", + "vue-router": "^4.6.4", + "vue3-apexcharts": "1.8.0", "vuedraggable": "4.1.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.0.3", - "autoprefixer": "^10.4.2", - "postcss": "^8.4.5", - "vite": "^5.0.11", - "vite-plugin-pwa": "^1.0.2" + "@vitejs/plugin-vue": "5.0.3", + "autoprefixer": "10.4.2", + "postcss": "8.4.5", + "tailwindcss": "^3.4.15", + "unplugin-auto-import": "^20.3.0", + "vite": "5.0.11", + "vite-plugin-pwa": "^1.2.0" } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a39b20fe..697aa346 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,18 +3,17 @@ - + diff --git a/frontend/src/assets/Inter/Inter-Black.woff b/frontend/src/assets/Inter/Inter-Black.woff deleted file mode 100644 index ab904b5f..00000000 Binary files a/frontend/src/assets/Inter/Inter-Black.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Black.woff2 b/frontend/src/assets/Inter/Inter-Black.woff2 deleted file mode 100644 index f7eec0f5..00000000 Binary files a/frontend/src/assets/Inter/Inter-Black.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BlackItalic.woff b/frontend/src/assets/Inter/Inter-BlackItalic.woff deleted file mode 100644 index ea5bb012..00000000 Binary files a/frontend/src/assets/Inter/Inter-BlackItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BlackItalic.woff2 b/frontend/src/assets/Inter/Inter-BlackItalic.woff2 deleted file mode 100644 index 5efefc6e..00000000 Binary files a/frontend/src/assets/Inter/Inter-BlackItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Bold.woff b/frontend/src/assets/Inter/Inter-Bold.woff deleted file mode 100644 index 993c3289..00000000 Binary files a/frontend/src/assets/Inter/Inter-Bold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Bold.woff2 b/frontend/src/assets/Inter/Inter-Bold.woff2 deleted file mode 100644 index 66412f0d..00000000 Binary files a/frontend/src/assets/Inter/Inter-Bold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BoldItalic.woff b/frontend/src/assets/Inter/Inter-BoldItalic.woff deleted file mode 100644 index a6b4f8ba..00000000 Binary files a/frontend/src/assets/Inter/Inter-BoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BoldItalic.woff2 b/frontend/src/assets/Inter/Inter-BoldItalic.woff2 deleted file mode 100644 index 486910d8..00000000 Binary files a/frontend/src/assets/Inter/Inter-BoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBold.woff b/frontend/src/assets/Inter/Inter-ExtraBold.woff deleted file mode 100644 index e2abc618..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBold.woff2 b/frontend/src/assets/Inter/Inter-ExtraBold.woff2 deleted file mode 100644 index fa4d0b7c..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff b/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff deleted file mode 100644 index b2d4b5f3..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 b/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 deleted file mode 100644 index e5403ff1..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLight.woff b/frontend/src/assets/Inter/Inter-ExtraLight.woff deleted file mode 100644 index 0c274676..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLight.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLight.woff2 b/frontend/src/assets/Inter/Inter-ExtraLight.woff2 deleted file mode 100644 index 52770bd3..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLight.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff b/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff deleted file mode 100644 index 1058b130..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 b/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 deleted file mode 100644 index a6dbd4ed..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Italic.woff b/frontend/src/assets/Inter/Inter-Italic.woff deleted file mode 100644 index 61355d63..00000000 Binary files a/frontend/src/assets/Inter/Inter-Italic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Italic.woff2 b/frontend/src/assets/Inter/Inter-Italic.woff2 deleted file mode 100644 index 06245e10..00000000 Binary files a/frontend/src/assets/Inter/Inter-Italic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Light.woff b/frontend/src/assets/Inter/Inter-Light.woff deleted file mode 100644 index 2f4ca350..00000000 Binary files a/frontend/src/assets/Inter/Inter-Light.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Light.woff2 b/frontend/src/assets/Inter/Inter-Light.woff2 deleted file mode 100644 index 7b8f903d..00000000 Binary files a/frontend/src/assets/Inter/Inter-Light.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-LightItalic.woff b/frontend/src/assets/Inter/Inter-LightItalic.woff deleted file mode 100644 index 880222e7..00000000 Binary files a/frontend/src/assets/Inter/Inter-LightItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-LightItalic.woff2 b/frontend/src/assets/Inter/Inter-LightItalic.woff2 deleted file mode 100644 index 8e332986..00000000 Binary files a/frontend/src/assets/Inter/Inter-LightItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Medium.woff b/frontend/src/assets/Inter/Inter-Medium.woff deleted file mode 100644 index ebcd5130..00000000 Binary files a/frontend/src/assets/Inter/Inter-Medium.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Medium.woff2 b/frontend/src/assets/Inter/Inter-Medium.woff2 deleted file mode 100644 index 73eb1b0d..00000000 Binary files a/frontend/src/assets/Inter/Inter-Medium.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-MediumItalic.woff b/frontend/src/assets/Inter/Inter-MediumItalic.woff deleted file mode 100644 index 8a2c2332..00000000 Binary files a/frontend/src/assets/Inter/Inter-MediumItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-MediumItalic.woff2 b/frontend/src/assets/Inter/Inter-MediumItalic.woff2 deleted file mode 100644 index c1e65ea9..00000000 Binary files a/frontend/src/assets/Inter/Inter-MediumItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Regular.woff b/frontend/src/assets/Inter/Inter-Regular.woff deleted file mode 100644 index b71e3ed1..00000000 Binary files a/frontend/src/assets/Inter/Inter-Regular.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Regular.woff2 b/frontend/src/assets/Inter/Inter-Regular.woff2 deleted file mode 100644 index 56f2155c..00000000 Binary files a/frontend/src/assets/Inter/Inter-Regular.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBold.woff b/frontend/src/assets/Inter/Inter-SemiBold.woff deleted file mode 100644 index 9306a14b..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBold.woff2 b/frontend/src/assets/Inter/Inter-SemiBold.woff2 deleted file mode 100644 index 41d32997..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff b/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff deleted file mode 100644 index c0a5287b..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 b/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 deleted file mode 100644 index d067fa93..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Thin.woff b/frontend/src/assets/Inter/Inter-Thin.woff deleted file mode 100644 index 95ba2012..00000000 Binary files a/frontend/src/assets/Inter/Inter-Thin.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Thin.woff2 b/frontend/src/assets/Inter/Inter-Thin.woff2 deleted file mode 100644 index dba7a520..00000000 Binary files a/frontend/src/assets/Inter/Inter-Thin.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ThinItalic.woff b/frontend/src/assets/Inter/Inter-ThinItalic.woff deleted file mode 100644 index 498906f9..00000000 Binary files a/frontend/src/assets/Inter/Inter-ThinItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ThinItalic.woff2 b/frontend/src/assets/Inter/Inter-ThinItalic.woff2 deleted file mode 100644 index c69ae0bd..00000000 Binary files a/frontend/src/assets/Inter/Inter-ThinItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-italic.var.woff2 b/frontend/src/assets/Inter/Inter-italic.var.woff2 deleted file mode 100644 index eb6399f9..00000000 Binary files a/frontend/src/assets/Inter/Inter-italic.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-roman.var.woff2 b/frontend/src/assets/Inter/Inter-roman.var.woff2 deleted file mode 100644 index 4cff8521..00000000 Binary files a/frontend/src/assets/Inter/Inter-roman.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter.var.woff2 b/frontend/src/assets/Inter/Inter.var.woff2 deleted file mode 100644 index 0c29f97c..00000000 Binary files a/frontend/src/assets/Inter/Inter.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/inter.css b/frontend/src/assets/Inter/inter.css deleted file mode 100644 index 3ca1bbf6..00000000 --- a/frontend/src/assets/Inter/inter.css +++ /dev/null @@ -1,152 +0,0 @@ -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 100; - font-display: swap; - src: url("Inter-Thin.woff2?v=3.12") format("woff2"), - url("Inter-Thin.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 100; - font-display: swap; - src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"), - url("Inter-ThinItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 200; - font-display: swap; - src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"), - url("Inter-ExtraLight.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 200; - font-display: swap; - src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"), - url("Inter-ExtraLightItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 300; - font-display: swap; - src: url("Inter-Light.woff2?v=3.12") format("woff2"), - url("Inter-Light.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 300; - font-display: swap; - src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"), - url("Inter-LightItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url("Inter-Regular.woff2?v=3.12") format("woff2"), - url("Inter-Regular.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 400; - font-display: swap; - src: url("Inter-Italic.woff2?v=3.12") format("woff2"), - url("Inter-Italic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url("Inter-Medium.woff2?v=3.12") format("woff2"), - url("Inter-Medium.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 500; - font-display: swap; - src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"), - url("Inter-MediumItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"), - url("Inter-SemiBold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 600; - font-display: swap; - src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-SemiBoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url("Inter-Bold.woff2?v=3.12") format("woff2"), - url("Inter-Bold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 700; - font-display: swap; - src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-BoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 800; - font-display: swap; - src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"), - url("Inter-ExtraBold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 800; - font-display: swap; - src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 900; - font-display: swap; - src: url("Inter-Black.woff2?v=3.12") format("woff2"), - url("Inter-Black.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 900; - font-display: swap; - src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"), - url("Inter-BlackItalic.woff?v=3.12") format("woff"); -} diff --git a/frontend/src/components/Annoucements.vue b/frontend/src/components/Annoucements.vue deleted file mode 100644 index 16ca3e41..00000000 --- a/frontend/src/components/Annoucements.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/frontend/src/components/AssessmentPlugin.vue b/frontend/src/components/AssessmentPlugin.vue index eae9763e..6f6a15f6 100644 --- a/frontend/src/components/AssessmentPlugin.vue +++ b/frontend/src/components/AssessmentPlugin.vue @@ -26,28 +26,52 @@ v-model="quiz" doctype="LMS Quiz" :label="__('Select a quiz')" + placeholder=" " :onCreate="(value, close) => redirectToForm()" /> - +
+ + + +
diff --git a/frontend/src/components/Assignment.vue b/frontend/src/components/Assignment.vue index 6c97c8d8..80611b05 100644 --- a/frontend/src/components/Assignment.vue +++ b/frontend/src/components/Assignment.vue @@ -16,8 +16,8 @@ {{ __('Submission by') }} {{ submissionResource.doc?.member_name }} -
- {{ __('Question') }}: +
+ {{ __('Assignment Question') }}
-
-
-
+
+
+
{{ __('Submission') }}
@@ -42,7 +42,11 @@ > {{ submissionResource.doc?.status }} -
@@ -53,7 +57,7 @@ !['Pass', 'Fail'].includes(submissionResource.doc?.status) && submissionResource.doc?.owner == user.data?.name " - class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm mb-4" + class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm" > {{ __("You've successfully submitted the assignment.") }} {{ @@ -63,17 +67,24 @@ }} {{ __('Feel free to make edits to your submission if needed.') }}
-
-
- {{ __('Add your assignment as {0}').format(assignment.data.type) }} +
+
+ {{ __('Upload Assignment') }} +
+
+ {{ + __('You can only upload {0} files').format(assignment.data.type) + }}
-
-
- -
+ @@ -142,13 +153,13 @@ user.data?.name == submissionResource.doc?.owner && submissionResource.doc?.comments " - class="mt-8 p-3 bg-surface-blue-2 rounded-md" + class="mt-8 p-3 border rounded-lg bg-surface-gray-2" > -
- {{ __('Comments by Evaluator') }}: +
+ {{ __('Comments by Evaluator') }}
@@ -179,7 +190,10 @@ " :editable="true" :fixedMenu="true" - editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]" + :uploadArgs="{ + private: true, + }" + editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]" />
@@ -201,11 +215,11 @@ import { } from 'frappe-ui' import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue' import { FileText, X } from 'lucide-vue-next' -import { getFileSize } from '@/utils' import { useRouter } from 'vue-router' +import { validateFile } from '@/utils' -const submissionFile = ref(null) const answer = ref(null) +const attachment = ref(null) const comments = ref(null) const router = useRouter() const user = inject('$user') @@ -255,129 +269,98 @@ const assignment = createResource({ }, }) -const newSubmission = createResource({ - url: 'frappe.client.insert', - makeParams(values) { - let doc = { - doctype: 'LMS Assignment Submission', - assignment: props.assignmentID, - member: user.data?.name, - } - if (showUploader()) { - doc.assignment_attachment = submissionFile.value.file_url - } else { - doc.answer = answer.value - } - return { - doc: doc, - } - }, -}) - -const imageResource = createResource({ - url: 'lms.lms.api.get_file_info', - makeParams(values) { - return { - file_url: values.image, - } - }, - auto: false, - onSuccess(data) { - submissionFile.value = data - }, -}) - const submissionResource = createDocumentResource({ doctype: 'LMS Assignment Submission', name: props.submissionName, + auto: false, onError(err) { toast.error(err.messages?.[0] || err) }, - auto: false, - cache: [user.data?.name, props.assignmentID], }) watch(submissionResource, () => { - if (submissionResource.doc) { - if (submissionResource.doc.assignment_attachment) { - imageResource.reload({ - image: submissionResource.doc.assignment_attachment, - }) - } - if (submissionResource.doc.answer) { - answer.value = submissionResource.doc.answer - } - if (submissionResource.doc.comments) { - comments.value = submissionResource.doc.comments - } - if (submissionResource.isDirty) { - isDirty.value = true - } else if (showUploader() && !submissionFile.value) { - isDirty.value = true - } else if (!showUploader() && !answer.value) { - isDirty.value = true - } else { - isDirty.value = false - } + if (!submissionResource.doc) return + console.log(submissionResource.doc) + if (submissionResource.doc.answer) { + answer.value = submissionResource.doc.answer } -}) - -watch(submissionFile, () => { - if (props.submissionName == 'new' && submissionFile.value) { - isDirty.value = true + if (submissionResource.doc.assignment_attachment) { + attachment.value = submissionResource.doc.assignment_attachment + } + if (submissionResource.doc.comments) { + comments.value = submissionResource.doc.comments } }) const submitAssignment = () => { if (props.submissionName != 'new') { - let evaluator = - submissionResource.doc && submissionResource.doc.owner != user.data?.name - ? user.data?.name - : null - - submissionResource.setValue.submit( - { - ...submissionResource.doc, - assignment_attachment: submissionFile.value?.file_url, - evaluator: evaluator, - comments: comments.value, - answer: answer.value, - }, - { - onSuccess(data) { - toast.success(__('Changes saved successfully')) - }, - } - ) + updateSubmission() } else { addNewSubmission() } } const addNewSubmission = () => { - newSubmission.submit( - {}, + let doc = { + doctype: 'LMS Assignment Submission', + assignment: props.assignmentID, + member: user.data?.name, + } + if (!showUploader()) { + doc.answer = answer.value + } else { + doc.assignment_attachment = attachment.value + } + 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() + } + isDirty.value = false + submissionResource.name = data.name + submissionResource.reload() + }) + .catch((err) => { + toast.error(err.messages?.[0] || err) + console.error(err) + }) +} + +const updateSubmission = () => { + let evaluator = + submissionResource.doc && submissionResource.doc.owner != user.data?.name + ? user.data?.name + : null + + submissionResource.setValue.submit( + { + ...submissionResource.doc, + evaluator: evaluator, + comments: comments.value, + answer: answer.value, + assignment_attachment: attachment.value, + }, { onSuccess(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() - } - submissionResource.name = data.name - submissionResource.reload() + isDirty.value = false + toast.success(__('Changes saved successfully')) }, onError(err) { toast.error(err.messages?.[0] || err) + console.error(err) }, } ) @@ -385,7 +368,7 @@ const addNewSubmission = () => { const saveSubmission = (file) => { isDirty.value = true - submissionFile.value = file + attachment.value = file.file_url } const markLessonProgress = () => { @@ -419,24 +402,9 @@ const getType = () => { } } -const validateFile = (file) => { - let type = assignment.data?.type - let extension = file.name.split('.').pop().toLowerCase() - if (type == 'Image' && !['jpg', 'jpeg', 'png'].includes(extension)) { - return 'Only image file is allowed.' - } else if ( - type == 'Document' && - !['doc', 'docx', 'xml'].includes(extension) - ) { - return 'Only document file is allowed.' - } else if (type == 'PDF' && !['pdf'].includes(extension)) { - return 'Only PDF file is allowed.' - } -} - const removeSubmission = () => { isDirty.value = true - submissionFile.value = null + submissionResource.doc.assignment_attachment = '' } const canGradeSubmission = computed(() => { diff --git a/frontend/src/components/BatchDashboard.vue b/frontend/src/components/BatchDashboard.vue deleted file mode 100644 index d7639490..00000000 --- a/frontend/src/components/BatchDashboard.vue +++ /dev/null @@ -1,26 +0,0 @@ - - diff --git a/frontend/src/components/BatchStudents.vue b/frontend/src/components/BatchStudents.vue deleted file mode 100644 index f3a73d76..00000000 --- a/frontend/src/components/BatchStudents.vue +++ /dev/null @@ -1,354 +0,0 @@ - - diff --git a/frontend/src/components/CertificationLinks.vue b/frontend/src/components/CertificationLinks.vue index 0da84a13..1f371def 100644 --- a/frontend/src/components/CertificationLinks.vue +++ b/frontend/src/components/CertificationLinks.vue @@ -68,11 +68,12 @@ const props = defineProps({ const certification = createResource({ url: 'lms.lms.api.get_certification_details', - params: { - course: props.courseName, + makeParams(values) { + return { + course: props.courseName, + } }, auto: user.data ? true : false, - cache: ['certificationData', user.data?.name], }) const downloadCertificate = () => { diff --git a/frontend/src/components/CommandPalette/CommandPalette.vue b/frontend/src/components/CommandPalette/CommandPalette.vue new file mode 100644 index 00000000..a32cbb00 --- /dev/null +++ b/frontend/src/components/CommandPalette/CommandPalette.vue @@ -0,0 +1,272 @@ + + + diff --git a/frontend/src/components/CommandPalette/CommandPaletteGroup.vue b/frontend/src/components/CommandPalette/CommandPaletteGroup.vue new file mode 100644 index 00000000..715b53ea --- /dev/null +++ b/frontend/src/components/CommandPalette/CommandPaletteGroup.vue @@ -0,0 +1,45 @@ + + diff --git a/frontend/src/components/ContactUsEmail.vue b/frontend/src/components/ContactUsEmail.vue index 04079d06..422ba847 100644 --- a/frontend/src/components/ContactUsEmail.vue +++ b/frontend/src/components/ContactUsEmail.vue @@ -48,7 +48,7 @@ const settingsStore = useSettings() const sendMail = (close: Function) => { call('frappe.core.doctype.communication.email.make', { - recipients: settingsStore.contactUsEmail?.data, + recipients: settingsStore.settings?.data?.contact_us_email, subject: subject.value, content: message.value, send_email: true, diff --git a/frontend/src/components/Controls/Autocomplete.vue b/frontend/src/components/Controls/Autocomplete.vue index d07fa67c..71739c20 100644 --- a/frontend/src/components/Controls/Autocomplete.vue +++ b/frontend/src/components/Controls/Autocomplete.vue @@ -16,13 +16,18 @@ -
-
+
+ + +
+ + + + +
+ +
@@ -73,17 +81,19 @@ diff --git a/frontend/src/components/Controls/Uploader.vue b/frontend/src/components/Controls/Uploader.vue index aff27c6e..1f6ec5ee 100644 --- a/frontend/src/components/Controls/Uploader.vue +++ b/frontend/src/components/Controls/Uploader.vue @@ -2,18 +2,21 @@
{{ __(label) }} - * + *
diff --git a/frontend/src/components/Modals/EditProfile.vue b/frontend/src/components/Modals/EditProfile.vue index 404cbc33..e1c993b9 100644 --- a/frontend/src/components/Modals/EditProfile.vue +++ b/frontend/src/components/Modals/EditProfile.vue @@ -1,91 +1,85 @@
-
    +
@@ -117,12 +117,21 @@ diff --git a/frontend/src/components/Settings/SettingDetails.vue b/frontend/src/components/Settings/SettingDetails.vue index 3319854c..3c276d1c 100644 --- a/frontend/src/components/Settings/SettingDetails.vue +++ b/frontend/src/components/Settings/SettingDetails.vue @@ -1,28 +1,30 @@ @@ -31,7 +33,7 @@ import { Button, Badge, toast } from 'frappe-ui' import SettingFields from '@/components/Settings/SettingFields.vue' const props = defineProps({ - fields: { + sections: { type: Array, required: true, }, @@ -49,13 +51,6 @@ const props = defineProps({ }) const update = () => { - props.fields.forEach((f) => { - if (f.type == 'Upload') { - props.data.doc[f.name] = f.value ? f.value.file_url : null - } else if (f.type != 'Column Break') { - props.data.doc[f.name] = f.value - } - }) props.data.save.submit( {}, { diff --git a/frontend/src/components/Settings/SettingFields.vue b/frontend/src/components/Settings/SettingFields.vue index 73f3b023..7dfacb3d 100644 --- a/frontend/src/components/Settings/SettingFields.vue +++ b/frontend/src/components/Settings/SettingFields.vue @@ -1,115 +1,123 @@ diff --git a/frontend/src/components/Settings/Settings.vue b/frontend/src/components/Settings/Settings.vue index fe8b2484..0365ed02 100644 --- a/frontend/src/components/Settings/Settings.vue +++ b/frontend/src/components/Settings/Settings.vue @@ -8,28 +8,30 @@

{{ __('Settings') }}

-
-
- {{ __(tab.label) }} -
- + +
- { return [ { - label: 'Settings', + label: 'Configuration', hideLabel: true, items: [ { label: 'General', icon: 'Wrench', - fields: [ + sections: [ { - label: 'Allow Guest Access', - name: 'allow_guest_access', - description: - 'If enabled, users can access the course and batch lists without logging in.', - type: 'checkbox', + label: 'System Configurations', + columns: [ + { + fields: [ + { + label: 'Allow Guest Access', + name: 'allow_guest_access', + description: + 'If enabled, users can access the course and batch lists without logging in.', + type: 'checkbox', + }, + { + label: 'Prevent Skipping Videos', + name: 'prevent_skipping_videos', + type: 'checkbox', + description: + 'If enabled, users will no able to move forward in a video', + }, + ], + }, + { + fields: [ + { + label: 'Disable PWA', + name: 'disable_pwa', + type: 'checkbox', + description: + 'If checked, users will not be able to install the application as a Progressive Web App.', + }, + { + label: 'Send calendar invite for evaluations', + name: 'send_calendar_invite_for_evaluations', + description: + 'If enabled, it sends google calendar invite to the student for evaluations.', + type: 'checkbox', + }, + ], + }, + ], }, { - label: 'Prevent Skipping Videos', - name: 'prevent_skipping_videos', - type: 'checkbox', - description: - 'If enabled, users will no able to move forward in a video', + label: 'Notifications', + columns: [ + { + fields: [ + { + label: 'Send Notification for Published Courses', + name: 'send_notification_for_published_courses', + type: 'select', + options: [' ', 'Email', 'In-app'], + }, + ], + }, + { + fields: [ + { + label: 'Send Notification for Published Batches', + name: 'send_notification_for_published_batches', + type: 'select', + options: [' ', 'Email', 'In-app'], + }, + ], + }, + ], }, { - label: 'Send calendar invite for evaluations', - name: 'send_calendar_invite_for_evaluations', - description: - 'If enabled, it sends google calendar invite to the student for evaluations.', - type: 'checkbox', + label: 'Email Templates', + columns: [ + { + fields: [ + { + label: 'Batch Confirmation Email Template', + name: 'batch_confirmation_template', + doctype: 'Email Template', + type: 'Link', + }, + ], + }, + { + fields: [ + { + label: 'Certification Email Template', + name: 'certification_template', + doctype: 'Email Template', + type: 'Link', + }, + ], + }, + ], }, { - type: 'Column Break', + label: 'Contact Information', + columns: [ + { + fields: [ + { + label: 'Email', + name: 'contact_us_email', + type: 'text', + description: + 'Users can reach out to this email for support or inquiries.', + }, + ], + }, + { + fields: [ + { + label: 'URL', + name: 'contact_us_url', + type: 'text', + description: + 'Users can reach out to this URL for support or inquiries.', + }, + ], + }, + ], }, { - label: 'Livecode URL', - name: 'livecode_url', - doctype: 'Livecode URL', - type: 'text', - description: - 'https://docs.frappe.io/learning/falcon-self-hosting-guide', + label: 'Jobs', + columns: [ + { + fields: [ + { + label: 'Allow Job Posting', + name: 'allow_job_posting', + type: 'checkbox', + description: + 'If enabled, users can post job openings on the job board. Else only admins can post jobs.', + }, + ], + }, + { + fields: [], + }, + ], }, { - label: 'Batch Confirmation Email Template', - name: 'batch_confirmation_template', - doctype: 'Email Template', - type: 'Link', - }, - { - label: 'Certification Email Template', - name: 'certification_template', - doctype: 'Email Template', - type: 'Link', - }, - { - label: 'Unsplash Access Key', - name: 'unsplash_access_key', - description: - 'Allows users to pick a profile cover image from Unsplash. https://unsplash.com/documentation#getting-started.', - type: 'password', + label: '', + columns: [ + { + fields: [ + { + label: 'Livecode URL', + name: 'livecode_url', + doctype: 'Livecode URL', + type: 'text', + description: + 'https://docs.frappe.io/learning/falcon-self-hosting-guide', + }, + ], + }, + { + fields: [ + { + label: 'Unsplash Access Key', + name: 'unsplash_access_key', + description: + 'Allows users to pick a profile cover image from Unsplash. https://unsplash.com/documentation#getting-started.', + type: 'password', + }, + ], + }, + ], }, ], }, - { - label: 'Contact Us', - icon: 'Phone', - fields: [ - { - label: 'Email', - name: 'contact_us_email', - type: 'text', - description: - 'Users can reach out to this email for support or inquiries.', - }, - { - label: 'URL', - name: 'contact_us_url', - type: 'text', - description: - 'Users can reach out to this URL for support or inquiries.', - }, - ], - }, - ], - }, - { - label: 'Payment', - hideLabel: false, - items: [ - { - label: 'Configuration', - icon: 'CreditCard', - description: 'Manage all your payment related settings and defaults', - fields: [ - { - label: 'Default Currency', - name: 'default_currency', - type: 'Link', - doctype: 'Currency', - }, - { - label: 'Payment Gateway', - name: 'payment_gateway', - type: 'Link', - doctype: 'Payment Gateway', - }, - { - type: 'Column Break', - }, - { - label: 'Apply GST for India', - name: 'apply_gst', - type: 'checkbox', - }, - { - label: 'Show USD equivalent amount', - name: 'show_usd_equivalent', - type: 'checkbox', - }, - { - label: 'Apply rounding on equivalent', - name: 'apply_rounding', - type: 'checkbox', - }, - ], - }, - { - label: 'Gateways', - icon: 'DollarSign', - template: markRaw(PaymentGateways), - description: 'Add and manage all your payment gateways', - }, - { - label: 'Transactions', - icon: 'Landmark', - template: markRaw(Transactions), - description: 'View all your payment transactions', - }, ], }, { @@ -282,6 +317,76 @@ const tabsStructure = computed(() => { }, ], }, + { + label: 'Payment', + hideLabel: false, + items: [ + { + label: 'Configuration', + icon: 'CreditCard', + description: 'Manage all your payment related settings and defaults', + sections: [ + { + columns: [ + { + fields: [ + { + label: 'Default Currency', + name: 'default_currency', + type: 'Link', + doctype: 'Currency', + }, + { + label: 'Payment Gateway', + name: 'payment_gateway', + type: 'Link', + doctype: 'Payment Gateway', + }, + ], + }, + { + fields: [ + { + label: 'Apply GST for India', + name: 'apply_gst', + type: 'checkbox', + }, + { + label: 'Show USD equivalent amount', + name: 'show_usd_equivalent', + type: 'checkbox', + }, + { + label: 'Apply rounding on equivalent', + name: 'apply_rounding', + type: 'checkbox', + }, + ], + }, + ], + }, + ], + }, + { + label: 'Gateways', + icon: 'DollarSign', + template: markRaw(PaymentGateways), + description: 'Add and manage all your payment gateways', + }, + { + label: 'Transactions', + icon: 'Landmark', + template: markRaw(Transactions), + description: 'View all your payment transactions', + }, + { + label: 'Coupons', + icon: 'Ticket', + template: markRaw(Coupons), + description: 'Manage discount coupons for courses and batches', + }, + ], + }, { label: 'Customize', hideLabel: false, @@ -290,25 +395,33 @@ const tabsStructure = computed(() => { label: 'Branding', icon: 'Blocks', template: markRaw(BrandSettings), - fields: [ + sections: [ { - label: 'Brand Name', - name: 'app_name', - type: 'text', - }, - { - label: 'Logo', - name: 'banner_image', - type: 'Upload', - description: - 'Appears in the top left corner of the application to represent your brand.', - }, - { - label: 'Favicon', - name: 'favicon', - type: 'Upload', - description: - 'Appears in the browser tab next to the page title to help users quickly identify the application.', + columns: [ + { + fields: [ + { + label: 'Brand Name', + name: 'app_name', + type: 'text', + }, + { + label: 'Logo', + name: 'banner_image', + type: 'Upload', + description: + 'Appears in the top left corner of the application to represent your brand.', + }, + { + label: 'Favicon', + name: 'favicon', + type: 'Upload', + description: + 'Appears in the browser tab next to the page title to help users quickly identify the application.', + }, + ], + }, + ], }, ], }, @@ -316,105 +429,124 @@ const tabsStructure = computed(() => { label: 'Sidebar', icon: 'PanelLeftIcon', description: 'Choose the items you want to show in the sidebar', - fields: [ + sections: [ { - label: 'Courses', - name: 'courses', - type: 'checkbox', - }, - { - label: 'Batches', - name: 'batches', - type: 'checkbox', - }, - { - label: 'Programming Exercises', - name: 'programming_exercises', - type: 'checkbox', - }, - { - label: 'Certifications', - name: 'certifications', - type: 'checkbox', - }, - { - type: 'Column Break', - }, - { - label: 'Jobs', - name: 'jobs', - type: 'checkbox', - }, - { - label: 'Statistics', - name: 'statistics', - type: 'checkbox', - }, - { - label: 'Notifications', - name: 'notifications', - type: 'checkbox', + columns: [ + { + fields: [ + { + label: 'Courses', + name: 'courses', + type: 'checkbox', + }, + { + label: 'Batches', + name: 'batches', + type: 'checkbox', + }, + { + label: 'Programming Exercises', + name: 'programming_exercises', + type: 'checkbox', + }, + { + label: 'Certifications', + name: 'certifications', + type: 'checkbox', + }, + ], + }, + { + fields: [ + { + label: 'Jobs', + name: 'jobs', + type: 'checkbox', + }, + { + label: 'Statistics', + name: 'statistics', + type: 'checkbox', + }, + { + label: 'Notifications', + name: 'notifications', + type: 'checkbox', + }, + ], + }, + ], }, ], }, { label: 'Signup', icon: 'LogIn', - fields: [ + sections: [ { - label: 'Identify User Category', - name: 'user_category', - type: 'checkbox', - description: - 'Enable this option to identify the user category during signup.', - }, - { - label: 'Disable signup', - name: 'disable_signup', - type: 'checkbox', - description: - 'New users will have to be manually registered by Admins.', - }, - { - type: 'Column Break', - }, - { - label: 'Signup Consent HTML', - name: 'custom_signup_content', - type: 'Code', - mode: 'htmlmixed', - rows: 10, + columns: [ + { + fields: [ + { + label: 'Identify User Category', + name: 'user_category', + type: 'checkbox', + description: + 'Enable this option to identify the user category during signup.', + }, + { + label: 'Disable signup', + name: 'disable_signup', + type: 'checkbox', + description: + 'New users will have to be manually registered by Admins.', + }, + { + label: 'Signup Consent HTML', + name: 'custom_signup_content', + type: 'Code', + mode: 'htmlmixed', + rows: 10, + }, + ], + }, + ], }, ], }, { label: 'SEO', icon: 'Search', - fields: [ + sections: [ { - label: 'Meta Description', - name: 'meta_description', - type: 'textarea', - rows: 4, - description: - "This description will be shown on lists and pages that don't have meta description", - }, - { - label: 'Meta Keywords', - name: 'meta_keywords', - type: 'textarea', - rows: 4, - description: - 'Comma separated keywords for search engines to find your website.', - }, - { - type: 'Column Break', - }, - { - label: 'Meta Image', - name: 'meta_image', - type: 'Upload', - size: 'lg', + columns: [ + { + fields: [ + { + label: 'Meta Description', + name: 'meta_description', + type: 'textarea', + rows: 4, + description: + "This description will be shown on lists and pages that don't have meta description", + }, + { + label: 'Meta Keywords', + name: 'meta_keywords', + type: 'textarea', + rows: 4, + description: + 'Comma separated keywords for search engines to find your website.', + }, + { + label: 'Meta Image', + name: 'meta_image', + type: 'Upload', + size: 'lg', + }, + ], + }, + ], }, ], }, diff --git a/frontend/src/components/Settings/TransactionDetails.vue b/frontend/src/components/Settings/TransactionDetails.vue deleted file mode 100644 index b996b02d..00000000 --- a/frontend/src/components/Settings/TransactionDetails.vue +++ /dev/null @@ -1,152 +0,0 @@ - - diff --git a/frontend/src/components/Settings/Transactions/TransactionDetails.vue b/frontend/src/components/Settings/Transactions/TransactionDetails.vue new file mode 100644 index 00000000..efd2cc88 --- /dev/null +++ b/frontend/src/components/Settings/Transactions/TransactionDetails.vue @@ -0,0 +1,272 @@ + + diff --git a/frontend/src/components/Settings/Transactions.vue b/frontend/src/components/Settings/Transactions/TransactionList.vue similarity index 78% rename from frontend/src/components/Settings/Transactions.vue rename to frontend/src/components/Settings/Transactions/TransactionList.vue index 0c6904fb..305b24a8 100644 --- a/frontend/src/components/Settings/Transactions.vue +++ b/frontend/src/components/Settings/Transactions/TransactionList.vue @@ -1,12 +1,20 @@ diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/Sidebar/AppSidebar.vue similarity index 79% rename from frontend/src/components/AppSidebar.vue rename to frontend/src/components/Sidebar/AppSidebar.vue index b85bcb8d..933f9011 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/Sidebar/AppSidebar.vue @@ -9,11 +9,21 @@ >
-
- +
+
+ {{ __(link.label) }} +
+
+ diff --git a/frontend/src/components/SidebarLink.vue b/frontend/src/components/Sidebar/SidebarLink.vue similarity index 100% rename from frontend/src/components/SidebarLink.vue rename to frontend/src/components/Sidebar/SidebarLink.vue diff --git a/frontend/src/components/UserDropdown.vue b/frontend/src/components/Sidebar/UserDropdown.vue similarity index 95% rename from frontend/src/components/UserDropdown.vue rename to frontend/src/components/Sidebar/UserDropdown.vue index 838123e5..1daef339 100644 --- a/frontend/src/components/UserDropdown.vue +++ b/frontend/src/components/Sidebar/UserDropdown.vue @@ -1,7 +1,7 @@