chore: resolved conflicts
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -71,6 +71,9 @@ jobs:
|
|||||||
- name: setup requirements
|
- name: setup requirements
|
||||||
working-directory: /home/runner/frappe-bench
|
working-directory: /home/runner/frappe-bench
|
||||||
run: bench setup requirements --dev
|
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
|
- name: allow tests
|
||||||
working-directory: /home/runner/frappe-bench
|
working-directory: /home/runner/frappe-bench
|
||||||
run: bench --site frappe.local set-config allow_tests true
|
run: bench --site frappe.local set-config allow_tests true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"branches": ["develop"],
|
"branches": ["main"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@semantic-release/commit-analyzer", {
|
"@semantic-release/commit-analyzer", {
|
||||||
"preset": "angular"
|
"preset": "angular"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ describe("Course Creation", () => {
|
|||||||
cy.get("button").contains("Create").click();
|
cy.get("button").contains("Create").click();
|
||||||
cy.get("span").contains("New Course").click();
|
cy.get("span").contains("New Course").click();
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.url().should("include", "/courses/new/edit");
|
|
||||||
|
|
||||||
cy.get("label").contains("Title").type("Test Course");
|
cy.get("label").contains("Title").type("Test Course");
|
||||||
cy.get("label")
|
cy.get("label")
|
||||||
@@ -35,21 +34,6 @@ 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 */
|
/* Instructor */
|
||||||
cy.get("label")
|
cy.get("label")
|
||||||
.contains("Instructors")
|
.contains("Instructors")
|
||||||
@@ -69,13 +53,32 @@ describe("Course Creation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.button("Create").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("[id^=headlessui-combobox-option-")
|
||||||
|
.should("be.visible")
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.get("label").contains("Published").click();
|
cy.get("label").contains("Published").click();
|
||||||
cy.get("label").contains("Published On").type("2021-01-01");
|
cy.get("label").contains("Published On").type("2021-01-01");
|
||||||
cy.button("Save").click();
|
cy.button("Save").click();
|
||||||
|
|
||||||
// Add Chapter
|
// Add Chapter
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.button("Add Chapter").click();
|
cy.button("Add").click();
|
||||||
|
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get("[data-dismissable-layer]")
|
cy.get("[data-dismissable-layer]")
|
||||||
|
|||||||
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@@ -54,7 +54,6 @@ declare module 'vue' {
|
|||||||
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
|
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
|
||||||
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
|
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
|
||||||
CourseOutline: typeof import('./src/components/CourseOutline.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']
|
CourseReviews: typeof import('./src/components/CourseReviews.vue')['default']
|
||||||
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
|
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
|
||||||
DateRange: typeof import('./src/components/Common/DateRange.vue')['default']
|
DateRange: typeof import('./src/components/Common/DateRange.vue')['default']
|
||||||
@@ -94,6 +93,7 @@ declare module 'vue' {
|
|||||||
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
|
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
|
||||||
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
|
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
|
||||||
NotPermitted: typeof import('./src/components/NotPermitted.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']
|
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
||||||
PaymentGatewayDetails: typeof import('./src/components/Settings/PaymentGatewayDetails.vue')['default']
|
PaymentGatewayDetails: typeof import('./src/components/Settings/PaymentGatewayDetails.vue')['default']
|
||||||
PaymentGateways: typeof import('./src/components/Settings/PaymentGateways.vue')['default']
|
PaymentGateways: typeof import('./src/components/Settings/PaymentGateways.vue')['default']
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
"@editorjs/paragraph": "2.11.3",
|
"@editorjs/paragraph": "2.11.3",
|
||||||
"@editorjs/simple-image": "1.6.0",
|
"@editorjs/simple-image": "1.6.0",
|
||||||
"@editorjs/table": "2.4.2",
|
"@editorjs/table": "2.4.2",
|
||||||
"@vueuse/core": "10.4.1",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/router": "12.7.0",
|
|
||||||
"ace-builds": "1.36.2",
|
"ace-builds": "1.36.2",
|
||||||
"apexcharts": "4.3.0",
|
"apexcharts": "4.3.0",
|
||||||
"chart.js": "4.4.1",
|
"chart.js": "4.4.1",
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
"dayjs": "1.11.10",
|
"dayjs": "1.11.10",
|
||||||
"dompurify": "3.2.6",
|
"dompurify": "3.2.6",
|
||||||
"feather-icons": "4.28.0",
|
"feather-icons": "4.28.0",
|
||||||
"frappe-ui": "^0.1.256",
|
"frappe-ui": "^0.1.261",
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"lucide-vue-next": "0.383.0",
|
"lucide-vue-next": "0.383.0",
|
||||||
"markdown-it": "14.0.0",
|
"markdown-it": "14.0.0",
|
||||||
@@ -43,11 +42,11 @@
|
|||||||
"socket.io-client": "4.7.2",
|
"socket.io-client": "4.7.2",
|
||||||
"thememirror": "2.0.1",
|
"thememirror": "2.0.1",
|
||||||
"typescript": "5.7.2",
|
"typescript": "5.7.2",
|
||||||
"vue": "^3.5.0",
|
"vue": "^3.5.27",
|
||||||
"vue-chartjs": "5.3.0",
|
"vue-chartjs": "5.3.0",
|
||||||
"vue-codemirror": "6.1.1",
|
"vue-codemirror": "6.1.1",
|
||||||
"vue-draggable-next": "2.2.1",
|
"vue-draggable-next": "2.2.1",
|
||||||
"vue-router": "4.2.2",
|
"vue-router": "^4.6.4",
|
||||||
"vue3-apexcharts": "1.8.0",
|
"vue3-apexcharts": "1.8.0",
|
||||||
"vuedraggable": "4.1.0"
|
"vuedraggable": "4.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { FrappeUIProvider } from 'frappe-ui'
|
import { FrappeUIProvider } from 'frappe-ui'
|
||||||
import { Dialogs } from '@/utils/dialogs'
|
import { Dialogs } from '@/utils/dialogs'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref } from 'vue'
|
||||||
import { useScreenSize } from './utils/composables'
|
import { useScreenSize } from './utils/composables'
|
||||||
import { usersStore } from '@/stores/user'
|
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import DesktopLayout from './components/DesktopLayout.vue'
|
import DesktopLayout from './components/DesktopLayout.vue'
|
||||||
@@ -23,7 +22,6 @@ import InstallPrompt from './components/InstallPrompt.vue'
|
|||||||
const { isMobile } = useScreenSize()
|
const { isMobile } = useScreenSize()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const noSidebar = ref(false)
|
const noSidebar = ref(false)
|
||||||
const { userResource } = usersStore()
|
|
||||||
const { settings } = useSettings()
|
const { settings } = useSettings()
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div class="p-5">
|
<div class="p-5 space-y-5">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between">
|
||||||
<div class="font-semibold text-ink-gray-9">
|
<div class="font-semibold text-ink-gray-9">
|
||||||
{{ __('Submission') }}
|
{{ __('Submission') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
!['Pass', 'Fail'].includes(submissionResource.doc?.status) &&
|
!['Pass', 'Fail'].includes(submissionResource.doc?.status) &&
|
||||||
submissionResource.doc?.owner == user.data?.name
|
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.") }}
|
{{ __("You've successfully submitted the assignment.") }}
|
||||||
{{
|
{{
|
||||||
@@ -63,12 +63,17 @@
|
|||||||
}}
|
}}
|
||||||
{{ __('Feel free to make edits to your submission if needed.') }}
|
{{ __('Feel free to make edits to your submission if needed.') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showUploader()">
|
<div v-if="showUploader()" class="border rounded-lg p-3">
|
||||||
<div class="text-xs text-ink-gray-5 mt-1 mb-2">
|
<div class="font-semibold mb-2">
|
||||||
{{ __('Add your assignment as {0}').format(assignment.data.type) }}
|
{{ __('Upload Assignment') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-5 text-sm mt-1 mb-4">
|
||||||
|
{{
|
||||||
|
__('You can only upload {0} files').format(assignment.data.type)
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
v-if="!submissionFile"
|
v-if="!submissionResource.doc?.assignment_attachment"
|
||||||
:fileTypes="getType()"
|
:fileTypes="getType()"
|
||||||
:uploadArgs="{
|
:uploadArgs="{
|
||||||
private: true,
|
private: true,
|
||||||
@@ -87,21 +92,24 @@
|
|||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex text-ink-gray-7">
|
<div class="flex items-center text-ink-gray-7">
|
||||||
<div class="border self-start rounded-md p-2 mr-2">
|
|
||||||
<FileText class="h-5 w-5 stroke-1.5" />
|
|
||||||
</div>
|
|
||||||
<a
|
<a
|
||||||
:href="submissionFile.file_url"
|
:href="submissionResource.doc.assignment_attachment"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex flex-col cursor-pointer !no-underline"
|
class="cursor-pointer !no-underline text-sm leading-5"
|
||||||
>
|
>
|
||||||
<span class="text-sm leading-5">
|
<div class="flex items-center">
|
||||||
{{ submissionFile.file_name }}
|
<div class="border rounded-md p-2 mr-2">
|
||||||
</span>
|
<FileText class="h-5 w-5 stroke-1.5" />
|
||||||
<span class="text-sm text-ink-gray-5 mt-1">
|
</div>
|
||||||
{{ getFileSize(submissionFile.file_size) }}
|
<span>
|
||||||
</span>
|
{{
|
||||||
|
submissionResource.doc.assignment_attachment
|
||||||
|
.split('/')
|
||||||
|
.pop()
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<X
|
<X
|
||||||
v-if="canModifyAssignment"
|
v-if="canModifyAssignment"
|
||||||
@@ -142,13 +150,13 @@
|
|||||||
user.data?.name == submissionResource.doc?.owner &&
|
user.data?.name == submissionResource.doc?.owner &&
|
||||||
submissionResource.doc?.comments
|
submissionResource.doc?.comments
|
||||||
"
|
"
|
||||||
class="mt-8 p-3 bg-surface-blue-2 rounded-md"
|
class="mt-8 p-3 border rounded-lg"
|
||||||
>
|
>
|
||||||
<div class="text-sm text-ink-gray-5 font-medium mb-2">
|
<div class="text-ink-gray-5 mb-4">
|
||||||
{{ __('Comments by Evaluator') }}:
|
{{ __('Comments by Evaluator') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="leading-5 text-ink-gray-9"
|
class="leading-6 text-ink-gray-9"
|
||||||
v-html="submissionResource.doc.comments"
|
v-html="submissionResource.doc.comments"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,10 +212,8 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
import { getFileSize } from '@/utils'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const submissionFile = ref(null)
|
|
||||||
const answer = ref(null)
|
const answer = ref(null)
|
||||||
const comments = ref(null)
|
const comments = ref(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -266,9 +272,7 @@ const newSubmission = createResource({
|
|||||||
assignment: props.assignmentID,
|
assignment: props.assignmentID,
|
||||||
member: user.data?.name,
|
member: user.data?.name,
|
||||||
}
|
}
|
||||||
if (showUploader()) {
|
if (!showUploader()) {
|
||||||
doc.assignment_attachment = submissionFile.value.file_url
|
|
||||||
} else {
|
|
||||||
doc.answer = answer.value
|
doc.answer = answer.value
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -277,19 +281,6 @@ const newSubmission = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
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({
|
const submissionResource = createDocumentResource({
|
||||||
doctype: 'LMS Assignment Submission',
|
doctype: 'LMS Assignment Submission',
|
||||||
name: props.submissionName,
|
name: props.submissionName,
|
||||||
@@ -302,11 +293,6 @@ const submissionResource = createDocumentResource({
|
|||||||
|
|
||||||
watch(submissionResource, () => {
|
watch(submissionResource, () => {
|
||||||
if (submissionResource.doc) {
|
if (submissionResource.doc) {
|
||||||
if (submissionResource.doc.assignment_attachment) {
|
|
||||||
imageResource.reload({
|
|
||||||
image: submissionResource.doc.assignment_attachment,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (submissionResource.doc.answer) {
|
if (submissionResource.doc.answer) {
|
||||||
answer.value = submissionResource.doc.answer
|
answer.value = submissionResource.doc.answer
|
||||||
}
|
}
|
||||||
@@ -315,7 +301,10 @@ watch(submissionResource, () => {
|
|||||||
}
|
}
|
||||||
if (submissionResource.isDirty) {
|
if (submissionResource.isDirty) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
} else if (showUploader() && !submissionFile.value) {
|
} else if (
|
||||||
|
showUploader() &&
|
||||||
|
!submissionResource.doc.assignment_attachment
|
||||||
|
) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
} else if (!showUploader() && !answer.value) {
|
} else if (!showUploader() && !answer.value) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
@@ -325,11 +314,17 @@ watch(submissionResource, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(submissionFile, () => {
|
watch(
|
||||||
if (props.submissionName == 'new' && submissionFile.value) {
|
() => submissionResource.doc,
|
||||||
isDirty.value = true
|
() => {
|
||||||
|
if (
|
||||||
|
props.submissionName == 'new' &&
|
||||||
|
submissionResource.doc?.assignment_attachment
|
||||||
|
) {
|
||||||
|
isDirty.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
const submitAssignment = () => {
|
const submitAssignment = () => {
|
||||||
if (props.submissionName != 'new') {
|
if (props.submissionName != 'new') {
|
||||||
@@ -341,13 +336,13 @@ const submitAssignment = () => {
|
|||||||
submissionResource.setValue.submit(
|
submissionResource.setValue.submit(
|
||||||
{
|
{
|
||||||
...submissionResource.doc,
|
...submissionResource.doc,
|
||||||
assignment_attachment: submissionFile.value?.file_url,
|
|
||||||
evaluator: evaluator,
|
evaluator: evaluator,
|
||||||
comments: comments.value,
|
comments: comments.value,
|
||||||
answer: answer.value,
|
answer: answer.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
isDirty.value = false
|
||||||
toast.success(__('Changes saved successfully'))
|
toast.success(__('Changes saved successfully'))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -388,7 +383,7 @@ const addNewSubmission = () => {
|
|||||||
|
|
||||||
const saveSubmission = (file) => {
|
const saveSubmission = (file) => {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
submissionFile.value = file
|
submissionResource.doc.assignment_attachment = file.file_url
|
||||||
}
|
}
|
||||||
|
|
||||||
const markLessonProgress = () => {
|
const markLessonProgress = () => {
|
||||||
@@ -439,7 +434,7 @@ const validateFile = (file) => {
|
|||||||
|
|
||||||
const removeSubmission = () => {
|
const removeSubmission = () => {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
submissionFile.value = null
|
submissionResource.doc.assignment_attachment = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const canGradeSubmission = computed(() => {
|
const canGradeSubmission = computed(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
||||||
<div
|
<div
|
||||||
v-if="batch.data.seat_count && seats_left > 0"
|
v-if="batch.data.seat_count && batch.data.seats_left > 0"
|
||||||
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
|
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
|
||||||
:class="
|
:class="
|
||||||
batch.data.amount || batch.data.courses.length
|
batch.data.amount || batch.data.courses.length
|
||||||
@@ -9,16 +9,16 @@
|
|||||||
: 'w-fit mb-4'
|
: 'w-fit mb-4'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ seats_left }}
|
{{ batch.data.seats_left }}
|
||||||
<span v-if="seats_left > 1">
|
<span v-if="batch.data.seats_left > 1">
|
||||||
{{ __('Seats Left') }}
|
{{ __('Seats Left') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="seats_left == 1">
|
<span v-else-if="batch.data.seats_left == 1">
|
||||||
{{ __('Seat Left') }}
|
{{ __('Seat Left') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="batch.data.seat_count && seats_left <= 0"
|
v-else-if="batch.data.seat_count && batch.data.seats_left <= 0"
|
||||||
class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
|
class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
|
||||||
>
|
>
|
||||||
{{ __('Sold Out') }}
|
{{ __('Sold Out') }}
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
{{ batch.data.timezone }}
|
{{ batch.data.timezone }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!readOnlyMode">
|
<div v-if="!readOnlyMode">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="canAccessBatch"
|
v-if="canAccessBatch"
|
||||||
@@ -190,15 +191,10 @@ const enrollInBatch = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const seats_left = computed(() => {
|
|
||||||
if (props.batch.data?.seat_count) {
|
|
||||||
return props.batch.data?.seat_count - props.batch.data?.students?.length
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const isStudent = computed(() => {
|
const isStudent = computed(() => {
|
||||||
return props.batch.data?.students?.includes(user.data?.name)
|
return user.data
|
||||||
|
? props.batch.data?.students?.includes(user.data?.name)
|
||||||
|
: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const isModerator = computed(() => {
|
const isModerator = computed(() => {
|
||||||
@@ -218,6 +214,9 @@ const isInstructor = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const canAccessBatch = computed(() => {
|
const canAccessBatch = computed(() => {
|
||||||
|
if (!user.data) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return isModerator.value || isStudent.value || isEvaluator.value
|
return isModerator.value || isStudent.value || isEvaluator.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<ListRow
|
<ListRow
|
||||||
:row="row"
|
:row="row"
|
||||||
v-for="row in students.data"
|
v-for="row in students.data"
|
||||||
class="group cursor-pointer"
|
class="group cursor-pointer hover:bg-surface-gray-2 rounded"
|
||||||
@click="openStudentProgressModal(row)"
|
@click="openStudentProgressModal(row)"
|
||||||
>
|
>
|
||||||
<template #default="{ column, item }">
|
<template #default="{ column, item }">
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
<div class="mt-4" v-if="students.hasNextPage">
|
<div class="mt-4 flex justify-center" v-if="students.hasNextPage">
|
||||||
<Button @click="students.next()">
|
<Button @click="students.next()">
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -170,7 +170,7 @@ const studentColumns = [
|
|||||||
{
|
{
|
||||||
label: 'Full Name',
|
label: 'Full Name',
|
||||||
key: 'full_name',
|
key: 'full_name',
|
||||||
width: '20rem',
|
width: '25rem',
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,9 +19,16 @@
|
|||||||
showOptions = true
|
showOptions = true
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
@click="
|
||||||
|
(e) => {
|
||||||
|
showOptions = true
|
||||||
|
nextTick(() => {
|
||||||
|
setFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
@focus="
|
@focus="
|
||||||
() => {
|
() => {
|
||||||
showOptions = true
|
|
||||||
if (!filterOptions.data || filterOptions.data.length === 0) {
|
if (!filterOptions.data || filterOptions.data.length === 0) {
|
||||||
reload('')
|
reload('')
|
||||||
}
|
}
|
||||||
@@ -33,10 +40,10 @@
|
|||||||
<template #body="{ isOpen, close }">
|
<template #body="{ isOpen, close }">
|
||||||
<div v-show="isOpen">
|
<div v-show="isOpen">
|
||||||
<div
|
<div
|
||||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
class="flex flex-col mt-1 rounded-lg bg-surface-white py-1 text-base border-2 max-h-[13rem]"
|
||||||
>
|
>
|
||||||
<ComboboxOptions
|
<ComboboxOptions
|
||||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
class="flex-1 my-1 overflow-y-auto px-1.5"
|
||||||
:class="options.length ? 'min-h-[6rem]' : 'min-h-[3.8rem]'"
|
:class="options.length ? 'min-h-[6rem]' : 'min-h-[3.8rem]'"
|
||||||
static
|
static
|
||||||
>
|
>
|
||||||
@@ -55,7 +62,11 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col gap-1 p-1">
|
<div class="flex flex-col gap-1 p-1">
|
||||||
<div class="text-base font-medium text-ink-gray-8">
|
<div class="text-base font-medium text-ink-gray-8">
|
||||||
{{ option.description }}
|
{{
|
||||||
|
option.value == option.label
|
||||||
|
? option.description
|
||||||
|
: option.label
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-ink-gray-5">
|
<div class="text-sm text-ink-gray-5">
|
||||||
{{ option.value }}
|
{{ option.value }}
|
||||||
@@ -66,22 +77,19 @@
|
|||||||
<div v-else class="text-ink-gray-7 px-4">
|
<div v-else class="text-ink-gray-7 px-4">
|
||||||
{{ __('No results found') }}
|
{{ __('No results found') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="attrs.onCreate"
|
|
||||||
class="absolute bottom-2 left-1 w-[95%] pt-2 bg-white border-t"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-full !justify-start"
|
|
||||||
:label="__('Create New')"
|
|
||||||
@click="attrs.onCreate(close)"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<Plus class="h-4 w-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComboboxOptions>
|
</ComboboxOptions>
|
||||||
|
<div v-if="attrs.onCreate" class="px-1 pt-2 bg-white border-t">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full !justify-start"
|
||||||
|
:label="__('Create New')"
|
||||||
|
@click="attrs.onCreate(close)"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -115,7 +123,7 @@ import {
|
|||||||
} from '@headlessui/vue'
|
} from '@headlessui/vue'
|
||||||
import { createResource, Popover, Button } from 'frappe-ui'
|
import { createResource, Popover, Button } from 'frappe-ui'
|
||||||
import { ref, computed, nextTick, useAttrs } from 'vue'
|
import { ref, computed, nextTick, useAttrs } from 'vue'
|
||||||
import { watchDebounced } from '@vueuse/core'
|
import { set, watchDebounced } from '@vueuse/core'
|
||||||
import { X, Plus } from 'lucide-vue-next'
|
import { X, Plus } from 'lucide-vue-next'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -149,18 +157,20 @@ const props = defineProps({
|
|||||||
|
|
||||||
const values = defineModel()
|
const values = defineModel()
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
const emails = ref([])
|
|
||||||
const search = ref(null)
|
const search = ref(null)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const showOptions = ref(false)
|
const showOptions = ref(false)
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const selectedValue = computed({
|
const selectedValue = computed({
|
||||||
get: () => query.value || '',
|
get: () => query.value || '',
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
query.value = ''
|
query.value = ''
|
||||||
val?.value && addValue(val.value)
|
val?.value && addValue(val.value)
|
||||||
|
showOptions.value = false
|
||||||
|
emit('update:modelValue', values.value)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -232,6 +242,7 @@ const addValue = (value) => {
|
|||||||
|
|
||||||
const removeValue = (value) => {
|
const removeValue = (value) => {
|
||||||
values.value = values.value.filter((v) => v !== value)
|
values.value = values.value.filter((v) => v !== value)
|
||||||
|
emit('update:modelValue', values.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFocus() {
|
function setFocus() {
|
||||||
|
|||||||
@@ -34,7 +34,12 @@
|
|||||||
<img
|
<img
|
||||||
v-if="type == 'image'"
|
v-if="type == 'image'"
|
||||||
:src="modelValue"
|
:src="modelValue"
|
||||||
class="border rounded-md w-44 h-auto"
|
:class="[
|
||||||
|
'border object-cover',
|
||||||
|
shape === 'circle'
|
||||||
|
? 'w-20 h-20 rounded-full'
|
||||||
|
: 'w-44 h-auto min-h-20 rounded-md',
|
||||||
|
]"
|
||||||
/>
|
/>
|
||||||
<video v-else controls class="border rounded-md w-44 h-auto">
|
<video v-else controls class="border rounded-md w-44 h-auto">
|
||||||
<source :src="modelValue" />
|
<source :src="modelValue" />
|
||||||
@@ -67,11 +72,12 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue: string
|
modelValue: string | null
|
||||||
label?: string
|
label?: string
|
||||||
description?: string
|
description?: string
|
||||||
type?: 'image' | 'video'
|
type?: 'image' | 'video'
|
||||||
required?: boolean
|
required?: boolean
|
||||||
|
shape?: 'square' | 'circle'
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
@@ -79,6 +85,7 @@ const props = withDefaults(
|
|||||||
description: '',
|
description: '',
|
||||||
type: 'image',
|
type: 'image',
|
||||||
required: true,
|
required: true,
|
||||||
|
shape: 'square',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<CertificationLinks :courseName="course.data.name" class="w-full" />
|
<CertificationLinks :courseName="course.data.name" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-else-if="course.data.paid_course"
|
v-else-if="course.data.paid_course && !isAdmin"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Billing',
|
name: 'Billing',
|
||||||
params: {
|
params: {
|
||||||
@@ -56,14 +56,15 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<Badge
|
<Badge
|
||||||
v-else-if="course.data.disable_self_learning"
|
v-else-if="course.data.disable_self_learning && !isAdmin"
|
||||||
theme="blue"
|
theme="blue"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
class="mb-4"
|
||||||
>
|
>
|
||||||
{{ __('Contact the Administrator to enroll for this course.') }}
|
{{ __('Contact the Administrator to enroll for this course') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="!user.data?.is_moderator && !is_instructor()"
|
v-else-if="!isAdmin"
|
||||||
@click="enrollStudent()"
|
@click="enrollStudent()"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -88,40 +89,11 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ __('Get Certificate') }}
|
{{ __('Get Certificate') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
v-if="user.data?.is_moderator || is_instructor()"
|
|
||||||
class="w-full mt-2"
|
|
||||||
size="md"
|
|
||||||
@click="showProgressSummary"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<TrendingUp class="size-4 stroke-1.5" />
|
|
||||||
{{ __('Progress Summary') }}
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<router-link
|
|
||||||
v-if="user?.data?.is_moderator || is_instructor()"
|
|
||||||
:to="{
|
|
||||||
name: 'CourseForm',
|
|
||||||
params: {
|
|
||||||
courseName: course.data.name,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Button variant="subtle" class="w-full mt-2" size="md">
|
|
||||||
<template #prefix>
|
|
||||||
<Pencil class="size-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
{{ __('Edit') }}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div
|
<div
|
||||||
class="font-medium text-ink-gray-9"
|
class="font-medium text-ink-gray-9"
|
||||||
:class="{ 'mt-8': !readOnlyMode }"
|
:class="{ 'mt-8': course.data.membership && !readOnlyMode }"
|
||||||
>
|
>
|
||||||
{{ __('This course has:') }}
|
{{ __('This course has:') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -168,12 +140,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CourseProgressSummary
|
|
||||||
v-if="user.data?.is_moderator || is_instructor()"
|
|
||||||
v-model="showProgressModal"
|
|
||||||
:courseName="course.data.name"
|
|
||||||
:enrollments="course.data.enrollments"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
@@ -191,12 +157,10 @@ import { Badge, Button, call, createResource, toast } from 'frappe-ui'
|
|||||||
import { formatAmount } from '@/utils/'
|
import { formatAmount } from '@/utils/'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
import CourseProgressSummary from '@/components/Modals/CourseProgressSummary.vue'
|
|
||||||
import { useTelemetry } from 'frappe-ui/frappe'
|
import { useTelemetry } from 'frappe-ui/frappe'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showProgressModal = ref(false)
|
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
const { capture } = useTelemetry()
|
const { capture } = useTelemetry()
|
||||||
|
|
||||||
@@ -216,7 +180,7 @@ const video_link = computed(() => {
|
|||||||
|
|
||||||
function enrollStudent() {
|
function enrollStudent() {
|
||||||
if (!user.data) {
|
if (!user.data) {
|
||||||
toast.success(__('You need to login first to enroll for this course'))
|
toast.warning(__('You need to login first to enroll for this course'))
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||||
}, 500)
|
}, 500)
|
||||||
@@ -295,7 +259,7 @@ const fetchCertificate = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showProgressSummary = () => {
|
const isAdmin = computed(() => {
|
||||||
showProgressModal.value = true
|
return user.data?.is_moderator || is_instructor()
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,7 +15,10 @@
|
|||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
||||||
{{ __('Add Chapter') }}
|
<template #prefix>
|
||||||
|
<Plus class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Add') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -174,6 +177,7 @@ import {
|
|||||||
FilePenLine,
|
FilePenLine,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
MonitorPlay,
|
MonitorPlay,
|
||||||
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|||||||
@@ -107,7 +107,11 @@
|
|||||||
v-model:reloadLiveClasses="liveClasses"
|
v-model:reloadLiveClasses="liveClasses"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LiveClassAttendance v-model="showAttendance" :live_class="attendanceFor" />
|
<LiveClassAttendance
|
||||||
|
v-if="showAttendance"
|
||||||
|
v-model="showAttendance"
|
||||||
|
:live_class="attendanceFor"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createListResource, Button, Tooltip } from 'frappe-ui'
|
import { createListResource, Button, Tooltip } from 'frappe-ui'
|
||||||
|
|||||||
@@ -23,10 +23,8 @@
|
|||||||
(value, close) => {
|
(value, close) => {
|
||||||
close()
|
close()
|
||||||
router.push({
|
router.push({
|
||||||
name: 'CourseForm',
|
name: 'Courses',
|
||||||
params: {
|
query: { newCourse: '1' },
|
||||||
courseName: 'new',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -1,231 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Dialog
|
|
||||||
v-model="show"
|
|
||||||
:options="{
|
|
||||||
title: __('Course Progress Summary'),
|
|
||||||
size: '5xl',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #body-content>
|
|
||||||
<div
|
|
||||||
class="flex flex-col-reverse md:flex-row justify-between md:space-x-10 text-base mt-10"
|
|
||||||
>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex items-center justify-between space-x-5 mb-4">
|
|
||||||
<FormControl
|
|
||||||
v-model="searchFilter"
|
|
||||||
:placeholder="__('Search by Member')"
|
|
||||||
type="text"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="max-h-[70vh] overflow-y-auto">
|
|
||||||
<ListView
|
|
||||||
v-if="progressList.loading || progressList.data?.length"
|
|
||||||
:columns="progressColumns"
|
|
||||||
:rows="progressList.data"
|
|
||||||
rowKey="name"
|
|
||||||
:options="{
|
|
||||||
selectable: false,
|
|
||||||
showTooltip: false,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<ListHeader
|
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
|
||||||
>
|
|
||||||
<ListHeaderItem
|
|
||||||
:item="item"
|
|
||||||
v-for="item in progressColumns"
|
|
||||||
:key="item.key"
|
|
||||||
>
|
|
||||||
<template #prefix="{ item }">
|
|
||||||
<FeatherIcon
|
|
||||||
:name="item.icon?.toString()"
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ListHeaderItem>
|
|
||||||
</ListHeader>
|
|
||||||
<ListRows v-for="row in progressList.data">
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'Profile',
|
|
||||||
params: { username: row.member_username },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<ListRow :row="row">
|
|
||||||
<template #default="{ column, item }">
|
|
||||||
<ListRowItem
|
|
||||||
:item="row[column.key]"
|
|
||||||
:align="column.align"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<div v-if="column.key == 'member_name'">
|
|
||||||
<Avatar
|
|
||||||
class="flex items-center"
|
|
||||||
:image="row['member_image']"
|
|
||||||
:label="item"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div>
|
|
||||||
{{ row[column.key].toString() }}
|
|
||||||
</div>
|
|
||||||
</ListRowItem>
|
|
||||||
</template>
|
|
||||||
</ListRow>
|
|
||||||
</router-link>
|
|
||||||
</ListRows>
|
|
||||||
</ListView>
|
|
||||||
<div
|
|
||||||
v-if="progressList.data && progressList.hasNextPage"
|
|
||||||
class="flex justify-center my-5"
|
|
||||||
>
|
|
||||||
<Button @click="progressList.next()">
|
|
||||||
{{ __('Load More') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4 self-start w-full space-y-5">
|
|
||||||
<div
|
|
||||||
class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4"
|
|
||||||
>
|
|
||||||
<NumberChart
|
|
||||||
class="border rounded-md w-full"
|
|
||||||
:config="{
|
|
||||||
title: __('Enrollments'),
|
|
||||||
value: memberCount || 0,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<NumberChart
|
|
||||||
class="border rounded-md w-full"
|
|
||||||
:config="{
|
|
||||||
title: __('Average Progress %'),
|
|
||||||
value: chartDetails.data?.average_progress || 0,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DonutChart
|
|
||||||
:config="{
|
|
||||||
data: chartDetails.data?.progress_distribution || [],
|
|
||||||
title: __('Progress Distribution'),
|
|
||||||
categoryColumn: 'category',
|
|
||||||
valueColumn: 'count',
|
|
||||||
colors: [
|
|
||||||
getColor('red', 400),
|
|
||||||
getColor('amber', 400),
|
|
||||||
getColor('pink', 400),
|
|
||||||
getColor('blue', 400),
|
|
||||||
getColor('green', 400),
|
|
||||||
],
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
createListResource,
|
|
||||||
createResource,
|
|
||||||
Dialog,
|
|
||||||
DonutChart,
|
|
||||||
FeatherIcon,
|
|
||||||
FormControl,
|
|
||||||
ListView,
|
|
||||||
ListHeader,
|
|
||||||
ListHeaderItem,
|
|
||||||
ListRows,
|
|
||||||
ListRow,
|
|
||||||
ListRowItem,
|
|
||||||
NumberChart,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { getColor } from '@/utils'
|
|
||||||
|
|
||||||
const show = defineModel<boolean>({ default: false })
|
|
||||||
const searchFilter = ref<string | null>(null)
|
|
||||||
type Filters = {
|
|
||||||
course: string | undefined
|
|
||||||
|
|
||||||
member_name?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
courseName?: string
|
|
||||||
enrollments?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const memberCount = ref<number>(props.enrollments || 0)
|
|
||||||
|
|
||||||
const chartDetails = createResource({
|
|
||||||
url: 'lms.lms.api.get_course_progress_distribution',
|
|
||||||
params: {
|
|
||||||
course: props.courseName,
|
|
||||||
},
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const progressList = createListResource({
|
|
||||||
doctype: 'LMS Enrollment',
|
|
||||||
filters: {
|
|
||||||
course: props.courseName,
|
|
||||||
},
|
|
||||||
fields: [
|
|
||||||
'name',
|
|
||||||
'member',
|
|
||||||
'member_name',
|
|
||||||
'member_image',
|
|
||||||
'member_username',
|
|
||||||
'progress',
|
|
||||||
],
|
|
||||||
pageLength: 50,
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
watch([searchFilter], () => {
|
|
||||||
let filterApplied = false
|
|
||||||
let filters: Filters = {
|
|
||||||
course: props.courseName,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchFilter.value) {
|
|
||||||
filters.member_name = ['like', `%${searchFilter.value}%`]
|
|
||||||
filterApplied = true
|
|
||||||
}
|
|
||||||
|
|
||||||
progressList.update({
|
|
||||||
filters: filters,
|
|
||||||
})
|
|
||||||
progressList.reload(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onSuccess(data: any[]) {
|
|
||||||
memberCount.value = filterApplied ? data.length : props.enrollments || 0
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const progressColumns = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: __('Member'),
|
|
||||||
key: 'member_name',
|
|
||||||
width: '60%',
|
|
||||||
icon: 'user',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __('Progress'),
|
|
||||||
key: 'progress',
|
|
||||||
align: 'right',
|
|
||||||
icon: 'trending-up',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
:options="{
|
:options="{
|
||||||
size: '3xl',
|
size: '3xl',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-header>
|
<template #body-header>
|
||||||
<div class="flex items-center mb-5">
|
<div class="flex items-center justify-between mb-5">
|
||||||
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||||
{{ __('Edit Profile') }}
|
{{ __('Edit Profile') }}
|
||||||
</div>
|
</div>
|
||||||
<Badge v-if="isDirty" class="ml-4" theme="orange">
|
<div class="space-x-2">
|
||||||
{{ __('Not Saved') }}
|
<Badge v-if="isDirty" theme="orange">
|
||||||
</Badge>
|
{{ __('Not Saved') }}
|
||||||
|
</Badge>
|
||||||
|
<div class="pb-5 float-right">
|
||||||
|
<Button variant="solid" @click="saveProfile()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
@@ -19,52 +27,13 @@
|
|||||||
<div class="grid grid-cols-2 gap-10">
|
<div class="grid grid-cols-2 gap-10">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<Uploader
|
||||||
<div class="text-xs text-ink-gray-5 mb-1">
|
v-model="profile.image"
|
||||||
{{ __('Profile Image') }}
|
:label="__('Profile Image')"
|
||||||
</div>
|
:required="true"
|
||||||
<FileUploader
|
shape="circle"
|
||||||
v-if="!profile.image"
|
/>
|
||||||
:fileTypes="['image/*']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => saveImage(file)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
|
||||||
>
|
|
||||||
<div class="mb-4">
|
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
|
||||||
{{
|
|
||||||
uploading
|
|
||||||
? `Uploading ${progress}%`
|
|
||||||
: 'Upload a profile image'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="mb-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="profile.image?.file_url"
|
|
||||||
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="text-base flex flex-col ml-2">
|
|
||||||
<span>
|
|
||||||
{{ profile.image?.file_name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-ink-gray-4 mt-1">
|
|
||||||
{{ getFileSize(profile.image?.file_size) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<X
|
|
||||||
@click="removeImage()"
|
|
||||||
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="profile.first_name"
|
v-model="profile.first_name"
|
||||||
:label="__('First Name')"
|
:label="__('First Name')"
|
||||||
@@ -90,7 +59,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-model="profile.open_to"
|
v-model="profile.open_to"
|
||||||
type="select"
|
type="select"
|
||||||
:options="[' ', 'Opportunities', 'Hiring']"
|
:options="[' ', 'Work', 'Hiring']"
|
||||||
:label="__('Open to')"
|
:label="__('Open to')"
|
||||||
:placeholder="__('Looking for new work or hiring talent?')"
|
:placeholder="__('Looking for new work or hiring talent?')"
|
||||||
/>
|
/>
|
||||||
@@ -115,13 +84,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ close }">
|
|
||||||
<div class="pb-5 float-right">
|
|
||||||
<Button variant="solid" @click="saveProfile(close)">
|
|
||||||
{{ __('Save') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -131,15 +93,14 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
Dialog,
|
Dialog,
|
||||||
FormControl,
|
FormControl,
|
||||||
FileUploader,
|
|
||||||
TextEditor,
|
TextEditor,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, reactive, watch } from 'vue'
|
import { ref, reactive, watch } from 'vue'
|
||||||
import { X } from 'lucide-vue-next'
|
import { sanitizeHTML } from '@/utils'
|
||||||
import { getFileSize, sanitizeHTML } from '@/utils'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
const reloadProfile = defineModel('reloadProfile')
|
const reloadProfile = defineModel('reloadProfile')
|
||||||
const hasLanguageChanged = ref(false)
|
const hasLanguageChanged = ref(false)
|
||||||
const isDirty = ref(false)
|
const isDirty = ref(false)
|
||||||
@@ -163,19 +124,6 @@ const profile = reactive({
|
|||||||
twitter: '',
|
twitter: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageResource = createResource({
|
|
||||||
url: 'lms.lms.api.get_file_info',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
file_url: values.image,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: false,
|
|
||||||
onSuccess(data) {
|
|
||||||
profile.image = data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateProfile = createResource({
|
const updateProfile = createResource({
|
||||||
url: 'frappe.client.set_value',
|
url: 'frappe.client.set_value',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
@@ -183,7 +131,7 @@ const updateProfile = createResource({
|
|||||||
doctype: 'User',
|
doctype: 'User',
|
||||||
name: props.profile.data.name,
|
name: props.profile.data.name,
|
||||||
fieldname: {
|
fieldname: {
|
||||||
user_image: profile.image?.file_url || null,
|
user_image: profile.image || null,
|
||||||
...profile,
|
...profile,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -193,13 +141,13 @@ const updateProfile = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveProfile = (close) => {
|
const saveProfile = () => {
|
||||||
profile.bio = sanitizeHTML(profile.bio)
|
profile.bio = sanitizeHTML(profile.bio)
|
||||||
updateProfile.submit(
|
updateProfile.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
close()
|
show.value = false
|
||||||
reloadProfile.value.reload()
|
reloadProfile.value.reload()
|
||||||
if (hasLanguageChanged.value) {
|
if (hasLanguageChanged.value) {
|
||||||
hasLanguageChanged.value = false
|
hasLanguageChanged.value = false
|
||||||
@@ -213,21 +161,6 @@ const saveProfile = (close) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateFile = (file) => {
|
|
||||||
let extension = file.name.split('.').pop().toLowerCase()
|
|
||||||
if (!['jpg', 'jpeg', 'png'].includes(extension)) {
|
|
||||||
return 'Only image file is allowed.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveImage = (file) => {
|
|
||||||
profile.image = file
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeImage = () => {
|
|
||||||
profile.image = null
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => profile,
|
() => profile,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@@ -240,7 +173,7 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (profile.image?.file_url !== props.profile.data.user_image) {
|
if (profile.image !== props.profile.data.user_image) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -262,7 +195,7 @@ watch(
|
|||||||
profile.linkedin = newVal.linkedin
|
profile.linkedin = newVal.linkedin
|
||||||
profile.github = newVal.github
|
profile.github = newVal.github
|
||||||
profile.twitter = newVal.twitter
|
profile.twitter = newVal.twitter
|
||||||
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
|
profile.image = newVal.user_image
|
||||||
isDirty.value = false
|
isDirty.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="__('Course')">
|
<Tooltip :text="__('Course')">
|
||||||
<div class="flex items-center space-x-2 w-fit">
|
<div
|
||||||
|
class="flex space-x-2 w-fit cursor-pointer"
|
||||||
|
@click="openLink('course', event.course)"
|
||||||
|
>
|
||||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
{{ event.course_title }}
|
{{ event.course_title }}
|
||||||
@@ -30,7 +33,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip v-if="event.batch_title" :text="__('Batch')">
|
<Tooltip v-if="event.batch_title" :text="__('Batch')">
|
||||||
<div class="flex items-center space-x-2 w-fit">
|
<div
|
||||||
|
class="flex space-x-2 w-fit cursor-pointer"
|
||||||
|
@click="openLink('batch', event.batch_name)"
|
||||||
|
>
|
||||||
<Users class="h-4 w-4 stroke-1.5" />
|
<Users class="h-4 w-4 stroke-1.5" />
|
||||||
<span>
|
<span>
|
||||||
{{ event.batch_title }}
|
{{ event.batch_title }}
|
||||||
@@ -334,7 +340,7 @@ const certificateDetails = createResource({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
certificate.template = defaultTemplate.data.value
|
certificate.template = defaultTemplate.data?.value
|
||||||
},
|
},
|
||||||
auto: false,
|
auto: false,
|
||||||
})
|
})
|
||||||
@@ -377,6 +383,16 @@ const openCertificate = (certificate) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openLink = (type, name) => {
|
||||||
|
let url = ''
|
||||||
|
if (type === 'course') {
|
||||||
|
url = `/lms/courses/${name}`
|
||||||
|
} else if (type === 'batch') {
|
||||||
|
url = `/lms/batches/${name}#students`
|
||||||
|
}
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
const statusOptions = computed(() => {
|
const statusOptions = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
20
frontend/src/components/NumberChartGraph.vue
Normal file
20
frontend/src/components/NumberChartGraph.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="border rounded-lg p-3 space-y-2">
|
||||||
|
<div class="text-ink-gray-5">
|
||||||
|
{{ __(title) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<slot name="prefix" />
|
||||||
|
<div class="font-semibold text-2xl">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<slot name="suffix" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string
|
||||||
|
value: number | string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tooltip :text="`${props.progress}%`">
|
<Tooltip :text="`${props.progress}%`">
|
||||||
<div class="w-full bg-surface-gray-3 rounded-full h-1">
|
<div
|
||||||
|
class="w-full bg-surface-gray-3 rounded-full h-1"
|
||||||
|
:class="$attrs.class"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-surface-gray-7 rounded-full"
|
class="bg-surface-gray-7 rounded-full"
|
||||||
:class="progressBarHeight"
|
:class="progressBarHeight"
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="font-semibold mb-1 text-ink-gray-9">
|
<div class="font-semibold mb-1 text-ink-gray-9">
|
||||||
{{ __(label) }}
|
{{ __(label) }}
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<div class="space-x-2">
|
||||||
v-if="isDirty"
|
<Badge
|
||||||
:label="__('Not Saved')"
|
v-if="isDirty"
|
||||||
variant="subtle"
|
:label="__('Not Saved')"
|
||||||
theme="orange"
|
variant="subtle"
|
||||||
/>
|
theme="orange"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
:loading="saveSettings.loading"
|
||||||
|
@click="update"
|
||||||
|
>
|
||||||
|
{{ __('Update') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-ink-gray-5">
|
<div class="text-xs text-ink-gray-5">
|
||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
@@ -19,11 +28,6 @@
|
|||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
<SettingFields :sections="sections" :data="branding.data" />
|
<SettingFields :sections="sections" :data="branding.data" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row-reverse mt-auto">
|
|
||||||
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
|
||||||
{{ __('Update') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -186,8 +186,9 @@ const openProfile = (username: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteEvaluator = (evaluator: string) => {
|
const deleteEvaluator = (evaluator: string) => {
|
||||||
call('lms.lms.api.delete_evaluator', {
|
call('frappe.client.delete', {
|
||||||
evaluator: evaluator,
|
doctype: 'Course Evaluator',
|
||||||
|
name: evaluator,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(__('Evaluator deleted successfully'))
|
toast.success(__('Evaluator deleted successfully'))
|
||||||
|
|||||||
@@ -2,21 +2,25 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:options="{
|
:options="{
|
||||||
title:
|
|
||||||
gatewayID === 'new'
|
|
||||||
? __('New Payment Gateway')
|
|
||||||
: __('Edit Payment Gateway'),
|
|
||||||
size: '3xl',
|
size: '3xl',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<template #body-header>
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
{{
|
||||||
|
gatewayID === 'new'
|
||||||
|
? __('New Payment Gateway')
|
||||||
|
: __('Edit Payment Gateway')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<SettingFields
|
<SettingFields
|
||||||
v-if="gatewayID != 'new' && paymentGateway.data"
|
v-if="gatewayID != 'new' && paymentGateway.data"
|
||||||
:fields="paymentGateway.data.fields"
|
:sections="paymentGateway.data.sections"
|
||||||
:data="paymentGateway.data.data"
|
:data="paymentGateway.data.data"
|
||||||
class="pt-5 my-0"
|
|
||||||
/>
|
/>
|
||||||
<div v-else>
|
<div v-else class="mt-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="newGateway"
|
v-model="newGateway"
|
||||||
:label="__('Select Payment Gateway')"
|
:label="__('Select Payment Gateway')"
|
||||||
@@ -26,9 +30,8 @@
|
|||||||
/>
|
/>
|
||||||
<SettingFields
|
<SettingFields
|
||||||
v-if="newGateway"
|
v-if="newGateway"
|
||||||
:fields="newGatewayFields"
|
:sections="newGatewayFields"
|
||||||
:data="newGatewayData"
|
:data="newGatewayData"
|
||||||
class="pt-5 my-0"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,7 +59,7 @@ import SettingFields from '@/components/Settings/SettingFields.vue'
|
|||||||
const show = defineModel<boolean>({ required: true, default: false })
|
const show = defineModel<boolean>({ required: true, default: false })
|
||||||
const paymentGateways = defineModel<any>('paymentGateways')
|
const paymentGateways = defineModel<any>('paymentGateways')
|
||||||
const newGateway = ref(null)
|
const newGateway = ref(null)
|
||||||
const newGatewayFields = ref([])
|
const newGatewayFields = ref<{ columns: { fields: any[] }[] }[]>([])
|
||||||
const newGatewayData = ref<Record<string, any>>({})
|
const newGatewayData = ref<Record<string, any>>({})
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -72,6 +75,7 @@ const paymentGateway = createResource({
|
|||||||
},
|
},
|
||||||
transform(data: any) {
|
transform(data: any) {
|
||||||
arrangeFields(data.fields)
|
arrangeFields(data.fields)
|
||||||
|
data.sections = makeSections(data.fields)
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -102,10 +106,6 @@ const arrangeFields = (fields: any[]) => {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
fields.splice(3, 0, {
|
|
||||||
type: 'Column Break',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -130,7 +130,7 @@ watch(newGateway, () => {
|
|||||||
gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => {
|
gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => {
|
||||||
let fields = gatewayFields.data || []
|
let fields = gatewayFields.data || []
|
||||||
arrangeFields(fields)
|
arrangeFields(fields)
|
||||||
newGatewayFields.value = fields
|
newGatewayFields.value = makeSections(fields)
|
||||||
prepareGatewayData()
|
prepareGatewayData()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -192,19 +192,6 @@ const getGatewayFields = () => {
|
|||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGatewayRecord = (gatewayDoc: any, data: any = {}) => {
|
|
||||||
call('frappe.client.insert', {
|
|
||||||
doc: {
|
|
||||||
doctype: 'Payment Gateway',
|
|
||||||
gateway: newGateway.value,
|
|
||||||
gateway_controller: gatewayDoc.issingle ? '' : gatewayDoc.name,
|
|
||||||
gateway_settings: gatewayDoc.issingle ? '' : data.name,
|
|
||||||
},
|
|
||||||
}).then(() => {
|
|
||||||
paymentGateways.value?.reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const allGatewayOptions = computed(() => {
|
const allGatewayOptions = computed(() => {
|
||||||
let options: string[] = []
|
let options: string[] = []
|
||||||
let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || []
|
let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || []
|
||||||
@@ -230,4 +217,20 @@ const prepareGatewayData = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const makeSections = (fields: any[]) => {
|
||||||
|
const columnCount = fields.length / 3
|
||||||
|
let sections: { columns: { fields: any[] }[] }[] = [
|
||||||
|
{
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let i = 0; i < columnCount; i++) {
|
||||||
|
sections[0].columns.push({
|
||||||
|
fields: fields.slice(i * 3, i * 3 + 3),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sections
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,22 +8,24 @@
|
|||||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
|
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div v-for="tab in tabs" :key="tab.label">
|
<div class="space-y-5">
|
||||||
<div
|
<div v-for="tab in tabs" :key="tab.label">
|
||||||
v-if="!tab.hideLabel"
|
<div
|
||||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
v-if="!tab.hideLabel"
|
||||||
>
|
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
||||||
<span>{{ __(tab.label) }}</span>
|
>
|
||||||
</div>
|
<span>{{ __(tab.label) }}</span>
|
||||||
<nav class="space-y-1">
|
|
||||||
<div v-for="item in tab.items" @click="activeTab = item">
|
|
||||||
<SidebarLink
|
|
||||||
:link="item"
|
|
||||||
:key="item.label"
|
|
||||||
:activeTab="activeTab?.label"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
<nav class="space-y-1">
|
||||||
|
<div v-for="item in tab.items" @click="activeTab = item">
|
||||||
|
<SidebarLink
|
||||||
|
:link="item"
|
||||||
|
:key="item.label"
|
||||||
|
:activeTab="activeTab?.label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full text-base">
|
<div class="flex flex-col h-full text-base">
|
||||||
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
|
<div class="flex items-center justify-between mb-10 -ml-1.5">
|
||||||
<ChevronLeft
|
<div class="flex items-center space-x-2">
|
||||||
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
<ChevronLeft
|
||||||
@click="emit('updateStep', 'list')"
|
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
||||||
/>
|
@click="emit('updateStep', 'list')"
|
||||||
<div class="text-xl font-semibold text-ink-gray-9">
|
/>
|
||||||
{{ __('Transaction Details') }}
|
<div class="text-xl font-semibold text-ink-gray-9">
|
||||||
|
{{ __('Transaction Details') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<Button
|
||||||
|
v-if="
|
||||||
|
transactionData?.payment_for_document_type &&
|
||||||
|
transactionData?.payment_for_document
|
||||||
|
"
|
||||||
|
@click="openDetails()"
|
||||||
|
>
|
||||||
|
{{ __('Open the ') }}
|
||||||
|
{{
|
||||||
|
transactionData.payment_for_document_type == 'LMS Course'
|
||||||
|
? __('Course')
|
||||||
|
: __('Batch')
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
<Button variant="solid" @click="saveTransaction()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="transactionData" class="overflow-y-auto">
|
<div v-if="transactionData" class="overflow-y-auto">
|
||||||
@@ -21,6 +42,12 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="transactionData.payment_for_certificate"
|
v-model="transactionData.payment_for_certificate"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
:label="__('Member Consent')"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="transactionData.member_consent"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-5 mt-5">
|
<div class="grid grid-cols-3 gap-5 mt-5">
|
||||||
@@ -28,22 +55,27 @@
|
|||||||
:label="__('Member')"
|
:label="__('Member')"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
v-model="transactionData.member"
|
v-model="transactionData.member"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
:label="__('Billing Name')"
|
:label="__('Billing Name')"
|
||||||
v-model="transactionData.billing_name"
|
v-model="transactionData.billing_name"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
:label="__('Source')"
|
:label="__('Source')"
|
||||||
v-model="transactionData.source"
|
v-model="transactionData.source"
|
||||||
doctype="LMS Source"
|
doctype="LMS Source"
|
||||||
/>
|
/>
|
||||||
<Link
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
:options="documentTypeOptions"
|
||||||
:label="__('Payment For Document Type')"
|
:label="__('Payment For Document Type')"
|
||||||
v-model="transactionData.payment_for_document_type"
|
v-model="transactionData.payment_for_document_type"
|
||||||
doctype="DocType"
|
doctype="DocType"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
|
v-if="transactionData.payment_for_document_type"
|
||||||
:label="__('Payment For Document')"
|
:label="__('Payment For Document')"
|
||||||
v-model="transactionData.payment_for_document"
|
v-model="transactionData.payment_for_document"
|
||||||
:doctype="transactionData.payment_for_document_type"
|
:doctype="transactionData.payment_for_document_type"
|
||||||
@@ -58,8 +90,13 @@
|
|||||||
:label="__('Currency')"
|
:label="__('Currency')"
|
||||||
v-model="transactionData.currency"
|
v-model="transactionData.currency"
|
||||||
doctype="Currency"
|
doctype="Currency"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
:label="__('Amount')"
|
||||||
|
v-model="transactionData.amount"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="transactionData.amount_with_gst"
|
v-if="transactionData.amount_with_gst"
|
||||||
:label="__('Amount with GST')"
|
:label="__('Amount with GST')"
|
||||||
@@ -103,6 +140,7 @@
|
|||||||
:label="__('Address')"
|
:label="__('Address')"
|
||||||
v-model="transactionData.address"
|
v-model="transactionData.address"
|
||||||
doctype="Address"
|
doctype="Address"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
|
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
|
||||||
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
|
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
|
||||||
@@ -116,25 +154,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2 mt-auto ml-auto">
|
|
||||||
<Button @click="openDetails()">
|
|
||||||
{{ __('Open the ') }}
|
|
||||||
{{
|
|
||||||
data.payment_for_document_type == 'LMS Course'
|
|
||||||
? __('Course')
|
|
||||||
: __('Batch')
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
<Button variant="solid" @click="saveTransaction()">
|
|
||||||
{{ __('Save') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Button, FormControl } from 'frappe-ui'
|
import { Button, FormControl, toast } from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { ChevronLeft } from 'lucide-vue-next'
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
@@ -148,21 +173,40 @@ const props = defineProps<{
|
|||||||
data: any
|
data: any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
watch(
|
const saveTransaction = () => {
|
||||||
() => props.data,
|
if (props.data?.name) {
|
||||||
(newVal) => {
|
updateTransaction()
|
||||||
transactionData.value = newVal ? { ...newVal } : null
|
} else {
|
||||||
},
|
createTransaction()
|
||||||
{ immediate: true }
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
const saveTransaction = (close: () => void) => {
|
const createTransaction = () => {
|
||||||
props.transactions.value.setValue
|
console.log(props.transactions)
|
||||||
|
props.transactions.insert
|
||||||
.submit({
|
.submit({
|
||||||
...transactionData.value,
|
...transactionData.value,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
close()
|
toast.success(__('Transaction created successfully'))
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
toast.error(__(err.messages?.[0] || err))
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTransaction = () => {
|
||||||
|
props.transactions.setValue
|
||||||
|
.submit({
|
||||||
|
...transactionData.value,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(__('Transaction updated successfully'))
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
toast.error(__(err.messages?.[0] || err))
|
||||||
|
console.error(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,4 +225,48 @@ const openDetails = () => {
|
|||||||
show.value = false
|
show.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyTransactionData = {
|
||||||
|
payment_received: false,
|
||||||
|
payment_for_certificate: false,
|
||||||
|
member: null,
|
||||||
|
billing_name: null,
|
||||||
|
source: null,
|
||||||
|
payment_for_document_type: null,
|
||||||
|
payment_for_document: null,
|
||||||
|
member_consent: false,
|
||||||
|
currency: null,
|
||||||
|
amount: null,
|
||||||
|
amount_with_gst: null,
|
||||||
|
coupon: null,
|
||||||
|
coupon_code: null,
|
||||||
|
discount_amount: null,
|
||||||
|
original_amount: null,
|
||||||
|
order_id: null,
|
||||||
|
payment_id: null,
|
||||||
|
gstin: null,
|
||||||
|
pan: null,
|
||||||
|
address: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newVal) => {
|
||||||
|
transactionData.value = newVal ? { ...newVal } : emptyTransactionData
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const documentTypeOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Course'),
|
||||||
|
value: 'LMS Course',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Batch'),
|
||||||
|
value: 'LMS Batch',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-0 flex-col text-base">
|
<div class="flex min-h-0 flex-col text-base">
|
||||||
<div class="mb-5">
|
<div class="flex items-center justify-between mb-5">
|
||||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
<div>
|
||||||
{{ __(label) }}
|
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||||
</div>
|
{{ __(label) }}
|
||||||
<div class="text-ink-gray-6 leading-5">
|
</div>
|
||||||
{{ __(description) }}
|
<div class="text-ink-gray-6 leading-5">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button @click="emit('updateStep', 'new', null)">
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Add Transaction') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-5 mb-4">
|
<div class="flex items-center space-x-5 mb-4">
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<TransactionDetails
|
||||||
|
v-if="step == 'new'"
|
||||||
|
:transactions="transactions"
|
||||||
|
:data="data"
|
||||||
|
v-model:show="show"
|
||||||
|
@updateStep="updateStep"
|
||||||
|
/>
|
||||||
<TransactionList
|
<TransactionList
|
||||||
v-if="step === 'list'"
|
v-else-if="step === 'list'"
|
||||||
:label="props.label"
|
:label="props.label"
|
||||||
:description="props.description"
|
:description="props.description"
|
||||||
:transactions="transactions"
|
:transactions="transactions"
|
||||||
@@ -33,6 +40,8 @@ const updateStep = (newStep: 'list' | 'new' | 'edit', newData: any) => {
|
|||||||
step.value = newStep
|
step.value = newStep
|
||||||
if (newData) {
|
if (newData) {
|
||||||
data.value = newData
|
data.value = newData
|
||||||
|
} else {
|
||||||
|
data.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,12 +269,13 @@ const iconProps = {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setUpOnboarding()
|
setUpOnboarding()
|
||||||
addKeyboardShortcut()
|
addKeyboardShortcut()
|
||||||
|
updateSidebarLinks()
|
||||||
socket.on('publish_lms_notifications', (data) => {
|
socket.on('publish_lms_notifications', (data) => {
|
||||||
unreadNotifications.reload()
|
unreadNotifications.reload()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const setSidebarLinks = () => {
|
const updateSidebarLinksVisibility = () => {
|
||||||
sidebarSettings.reload(
|
sidebarSettings.reload(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -405,9 +406,13 @@ const steps = reactive([
|
|||||||
minimize.value = true
|
minimize.value = true
|
||||||
let course = await getFirstCourse()
|
let course = await getFirstCourse()
|
||||||
if (course) {
|
if (course) {
|
||||||
router.push({ name: 'CourseForm', params: { courseName: course } })
|
router.push({
|
||||||
|
name: 'CourseDetail',
|
||||||
|
params: { courseName: course },
|
||||||
|
hash: '#settings',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
router.push({ name: 'CourseForm' })
|
router.push({ name: 'Courses', query: { newCourse: '1' } })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -422,11 +427,12 @@ const steps = reactive([
|
|||||||
let course = await getFirstCourse()
|
let course = await getFirstCourse()
|
||||||
if (course) {
|
if (course) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'CourseForm',
|
name: 'CourseDetail',
|
||||||
params: { courseName: course },
|
params: { courseName: course },
|
||||||
|
hash: '#settings',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
router.push({ name: 'Courses' })
|
router.push({ name: 'Courses', query: { newCourse: '1' } })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -591,10 +597,18 @@ watch(userResource, async () => {
|
|||||||
await programs.reload()
|
await programs.reload()
|
||||||
setUpOnboarding()
|
setUpOnboarding()
|
||||||
}
|
}
|
||||||
sidebarLinks.value = getSidebarLinks()
|
updateSidebarLinks()
|
||||||
setSidebarLinks()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(settingsStore.settings, () => {
|
||||||
|
updateSidebarLinks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSidebarLinks = () => {
|
||||||
|
sidebarLinks.value = getSidebarLinks()
|
||||||
|
updateSidebarLinksVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
const redirectToWebsite = () => {
|
const redirectToWebsite = () => {
|
||||||
window.open('https://frappe.io/learning', '_blank')
|
window.open('https://frappe.io/learning', '_blank')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ const evaluationCourses = computed(() => {
|
|||||||
|
|
||||||
const canScheduleEvals = computed(() => {
|
const canScheduleEvals = computed(() => {
|
||||||
return (
|
return (
|
||||||
upcoming_evals.data?.length != evaluationCourses.length &&
|
upcoming_evals.data?.length != evaluationCourses.value?.length &&
|
||||||
!props.forHome &&
|
!props.forHome &&
|
||||||
!endDateHasPassed.value
|
!endDateHasPassed.value
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
:size="size"
|
:size="size"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<template v-if="user.open_to === 'Opportunities'" #indicator>
|
<template v-if="user.open_to === 'Work'" #indicator>
|
||||||
<Tooltip :text="__('Open to Opportunities')" placement="right">
|
<Tooltip :text="__('Open to Work')" placement="right">
|
||||||
<div class="rounded-full bg-surface-green-3 w-fit">
|
<div class="rounded-full bg-surface-green-3 w-fit">
|
||||||
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
|
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
@import './assets/Inter/inter.css';
|
|
||||||
@import 'frappe-ui/style.css';
|
@import 'frappe-ui/style.css';
|
||||||
@import './styles/codemirror.css';
|
@import './styles/codemirror.css';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import { createApp } from 'vue'
|
import { createApp, watch } from 'vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
@@ -19,7 +19,6 @@ app.use(FrappeUI)
|
|||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(translationPlugin)
|
app.use(translationPlugin)
|
||||||
app.use(telemetryPlugin, { app_name: 'lms' })
|
|
||||||
app.use(pageMetaPlugin)
|
app.use(pageMetaPlugin)
|
||||||
app.provide('$dayjs', dayjs)
|
app.provide('$dayjs', dayjs)
|
||||||
app.provide('$socket', initSocket())
|
app.provide('$socket', initSocket())
|
||||||
@@ -29,5 +28,11 @@ const { userResource, allUsers } = usersStore()
|
|||||||
app.provide('$user', userResource)
|
app.provide('$user', userResource)
|
||||||
app.provide('$allUsers', allUsers)
|
app.provide('$allUsers', allUsers)
|
||||||
|
|
||||||
|
watch(userResource, () => {
|
||||||
|
if (userResource.data) {
|
||||||
|
app.use(telemetryPlugin, { app_name: 'lms' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.config.globalProperties.$user = userResource
|
app.config.globalProperties.$user = userResource
|
||||||
app.config.globalProperties.$dialog = createDialog
|
app.config.globalProperties.$dialog = createDialog
|
||||||
|
|||||||
@@ -130,7 +130,6 @@
|
|||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
call,
|
|
||||||
createListResource,
|
createListResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -185,24 +184,27 @@ const batches = createListResource({
|
|||||||
cache: ['batches', user.data?.name],
|
cache: ['batches', user.data?.name],
|
||||||
pageLength: pageLength.value,
|
pageLength: pageLength.value,
|
||||||
start: start.value,
|
start: start.value,
|
||||||
onSuccess(data) {
|
|
||||||
let allCategories = data.map((batch) => batch.category)
|
|
||||||
allCategories = allCategories.filter(
|
|
||||||
(category, index) => allCategories.indexOf(category) === index && category
|
|
||||||
)
|
|
||||||
if (categories.value.length <= allCategories.length) {
|
|
||||||
updateCategories(data)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const setCategories = (data) => {
|
||||||
|
let allCategories = data.map((batch) => batch.category)
|
||||||
|
allCategories = allCategories.filter(
|
||||||
|
(category, index) => allCategories.indexOf(category) === index && category
|
||||||
|
)
|
||||||
|
if (categories.value.length <= allCategories.length) {
|
||||||
|
updateCategories(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateBatches = () => {
|
const updateBatches = () => {
|
||||||
updateFilters()
|
updateFilters()
|
||||||
batches.update({
|
batches.update({
|
||||||
filters: filters.value,
|
filters: filters.value,
|
||||||
orderBy: orderBy.value,
|
orderBy: orderBy.value,
|
||||||
})
|
})
|
||||||
batches.reload()
|
batches.reload().then((data) => {
|
||||||
|
setCategories(data)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<router-link :to="{ name: 'Batches', query: { certification: true } }">
|
<router-link :to="{ name: 'Courses', query: { certification: true } }">
|
||||||
<Button>
|
<Button>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<GraduationCap class="h-4 w-4 stroke-1.5" />
|
<GraduationCap class="h-4 w-4 stroke-1.5" />
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="openToOpportunities"
|
v-model="openToWork"
|
||||||
:label="__('Open to Opportunities')"
|
:label="__('Open to Work')"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@change="updateParticipants()"
|
@change="updateParticipants()"
|
||||||
/>
|
/>
|
||||||
@@ -134,19 +134,26 @@ import {
|
|||||||
import { computed, inject, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
import { GraduationCap } from 'lucide-vue-next'
|
import { GraduationCap } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
|
||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const currentCategory = ref('')
|
const currentCategory = ref('')
|
||||||
const nameFilter = ref('')
|
const nameFilter = ref('')
|
||||||
const openToOpportunities = ref(false)
|
const openToWork = ref(false)
|
||||||
const hiring = ref(false)
|
const hiring = ref(false)
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const memberCount = ref(0)
|
const memberCount = ref(0)
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
|
const user = inject('$user')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (!user.data) {
|
||||||
|
router.push({ name: 'Courses' })
|
||||||
|
return
|
||||||
|
}
|
||||||
setFiltersFromQuery()
|
setFiltersFromQuery()
|
||||||
updateParticipants()
|
updateParticipants()
|
||||||
})
|
})
|
||||||
@@ -171,7 +178,7 @@ const categories = createListResource({
|
|||||||
doctype: 'LMS Certificate',
|
doctype: 'LMS Certificate',
|
||||||
url: 'lms.lms.api.get_certification_categories',
|
url: 'lms.lms.api.get_certification_categories',
|
||||||
cache: ['certification_categories'],
|
cache: ['certification_categories'],
|
||||||
auto: true,
|
auto: user.data ? true : false,
|
||||||
transform(data) {
|
transform(data) {
|
||||||
data.unshift({ label: __(' '), value: ' ' })
|
data.unshift({ label: __(' '), value: ' ' })
|
||||||
return data
|
return data
|
||||||
@@ -197,8 +204,8 @@ const updateFilters = () => {
|
|||||||
...(nameFilter.value && {
|
...(nameFilter.value && {
|
||||||
member_name: ['like', `%${nameFilter.value}%`],
|
member_name: ['like', `%${nameFilter.value}%`],
|
||||||
}),
|
}),
|
||||||
...(openToOpportunities.value && {
|
...(openToWork.value && {
|
||||||
open_to_opportunities: true,
|
open_to_work: true,
|
||||||
}),
|
}),
|
||||||
...(hiring.value && {
|
...(hiring.value && {
|
||||||
hiring: true,
|
hiring: true,
|
||||||
@@ -211,7 +218,7 @@ const setQueryParams = () => {
|
|||||||
let filterKeys = {
|
let filterKeys = {
|
||||||
category: currentCategory.value,
|
category: currentCategory.value,
|
||||||
name: nameFilter.value,
|
name: nameFilter.value,
|
||||||
'open-to-opportunities': openToOpportunities.value,
|
'open-to-work': openToWork.value,
|
||||||
hiring: hiring.value,
|
hiring: hiring.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +247,7 @@ const setFiltersFromQuery = () => {
|
|||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
nameFilter.value = queries.get('name') || ''
|
nameFilter.value = queries.get('name') || ''
|
||||||
currentCategory.value = queries.get('category') || ''
|
currentCategory.value = queries.get('category') || ''
|
||||||
openToOpportunities.value = queries.get('open-to-opportunities') === 'true'
|
openToWork.value = queries.get('open-to-opportunities') === 'true'
|
||||||
hiring.value = queries.get('hiring') === 'true'
|
hiring.value = queries.get('hiring') === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="course.data">
|
|
||||||
<header
|
|
||||||
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" />
|
|
||||||
</header>
|
|
||||||
<div class="m-5">
|
|
||||||
<div class="flex justify-between w-full space-x-5">
|
|
||||||
<div class="md:w-2/3">
|
|
||||||
<div class="text-3xl font-semibold text-ink-gray-9">
|
|
||||||
{{ course.data.title }}
|
|
||||||
</div>
|
|
||||||
<div class="my-3 leading-6 text-ink-gray-7">
|
|
||||||
{{ course.data.short_introduction }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Tooltip
|
|
||||||
v-if="parseInt(course.data.rating) > 0"
|
|
||||||
:text="__('Average Rating')"
|
|
||||||
class="flex items-center"
|
|
||||||
>
|
|
||||||
<Star class="size-4 text-transparent fill-yellow-500" />
|
|
||||||
<span class="ml-1 text-ink-gray-7">
|
|
||||||
{{ course.data.rating }}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<span v-if="parseInt(course.data.rating) > 0" class="mx-3"
|
|
||||||
>·</span
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
v-if="course.data.enrollment_count"
|
|
||||||
:text="__('Enrolled Students')"
|
|
||||||
class="flex items-center"
|
|
||||||
>
|
|
||||||
<Users class="h-4 w-4 text-ink-gray-7" />
|
|
||||||
<span class="ml-1">
|
|
||||||
{{ course.data.enrollment_count_formatted }}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<span v-if="course.data.enrollment_count" class="mx-3"
|
|
||||||
>·</span
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span
|
|
||||||
class="h-6 mr-1"
|
|
||||||
:class="{
|
|
||||||
'avatar-group overlap': course.data.instructors.length > 1,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<UserAvatar
|
|
||||||
v-for="instructor in course.data.instructors"
|
|
||||||
:user="instructor"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<CourseInstructors :instructors="course.data.instructors" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="course.data.tags" class="flex my-4 w-fit">
|
|
||||||
<Badge
|
|
||||||
theme="gray"
|
|
||||||
size="lg"
|
|
||||||
class="mr-2 text-ink-gray-9"
|
|
||||||
v-for="tag in course.data.tags.split(', ')"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="md:hidden my-4">
|
|
||||||
<CourseCardOverlay :course="course" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-html="course.data.description"
|
|
||||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
|
|
||||||
></div>
|
|
||||||
<div class="mt-10">
|
|
||||||
<CourseOutline
|
|
||||||
:title="__('Course Outline')"
|
|
||||||
:courseName="course.data.name"
|
|
||||||
:showOutline="true"
|
|
||||||
:getProgress="course.data.membership ? true : false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<CourseReviews
|
|
||||||
:courseName="course.data.name"
|
|
||||||
:avg_rating="course.data.rating"
|
|
||||||
:membership="course.data.membership"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="hidden md:block">
|
|
||||||
<CourseCardOverlay :course="course" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<RelatedCourses :courseName="course.data.name" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
createResource,
|
|
||||||
Breadcrumbs,
|
|
||||||
Badge,
|
|
||||||
Tooltip,
|
|
||||||
usePageMeta,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { computed, inject, watch } from 'vue'
|
|
||||||
import { Users, Star } from 'lucide-vue-next'
|
|
||||||
import { sessionStore } from '@/stores/session'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import CourseCardOverlay from '@/components/CourseCardOverlay.vue'
|
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
|
||||||
import CourseReviews from '@/components/CourseReviews.vue'
|
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
|
||||||
import RelatedCourses from '@/components/RelatedCourses.vue'
|
|
||||||
|
|
||||||
const { brand } = sessionStore()
|
|
||||||
const router = useRouter()
|
|
||||||
const user = inject('$user')
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
courseName: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const course = createResource({
|
|
||||||
url: 'lms.lms.utils.get_course_details',
|
|
||||||
cache: ['course', props.courseName],
|
|
||||||
makeParams() {
|
|
||||||
return {
|
|
||||||
course: props.courseName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.courseName,
|
|
||||||
() => {
|
|
||||||
course.reload()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(course, () => {
|
|
||||||
if (
|
|
||||||
!isInstructor() &&
|
|
||||||
!user.data?.is_moderator &&
|
|
||||||
!course.data?.published &&
|
|
||||||
!course.data?.upcoming
|
|
||||||
) {
|
|
||||||
router.push({
|
|
||||||
name: 'Courses',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isInstructor = () => {
|
|
||||||
let user_is_instructor = false
|
|
||||||
course.data?.instructors.forEach((instructor) => {
|
|
||||||
if (!user_is_instructor && instructor.name == user.data?.name) {
|
|
||||||
user_is_instructor = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return user_is_instructor
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
|
|
||||||
items.push({
|
|
||||||
label: course?.data?.title,
|
|
||||||
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
})
|
|
||||||
|
|
||||||
usePageMeta(() => {
|
|
||||||
return {
|
|
||||||
title: course?.data?.title,
|
|
||||||
icon: brand.favicon,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.avatar-group {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-group .avatar {
|
|
||||||
transition: margin 0.1s ease-in-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
import { computed, inject, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
import { Breadcrumbs, call, createResource, usePageMeta } from 'frappe-ui'
|
import { Breadcrumbs, call, createResource, usePageMeta } from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../../stores/session'
|
||||||
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||||
|
|
||||||
const courseTitle = ref(null)
|
const courseTitle = ref(null)
|
||||||
400
frontend/src/pages/Courses/CourseDashboard.vue
Normal file
400
frontend/src/pages/Courses/CourseDashboard.vue
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="grid grid-cols-4 gap-5 mb-5">
|
||||||
|
<NumberChartGraph
|
||||||
|
:title="__('Enrolled')"
|
||||||
|
:value="formatAmount(course.data?.enrollments)"
|
||||||
|
/>
|
||||||
|
<NumberChartGraph
|
||||||
|
:title="__('Average Completion Rate')"
|
||||||
|
:value="averageCompletionRate"
|
||||||
|
/>
|
||||||
|
<NumberChartGraph
|
||||||
|
:title="__('Average Rating')"
|
||||||
|
:value="course.data?.rating || 0"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Star class="size-5 text-transparent fill-amber-500" />
|
||||||
|
</template>
|
||||||
|
</NumberChartGraph>
|
||||||
|
<NumberChartGraph :title="__('Lessons')" :value="course.data?.lessons" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-[2fr_1fr] gap-5 items-start">
|
||||||
|
<div v-if="course.data?.enrollments" class="border rounded-lg py-3 px-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
{{ __('Students') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<FormControl
|
||||||
|
v-model="searchFilter"
|
||||||
|
:placeholder="__('Search by name')"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<Button @click="showEnrollmentModal = true">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Enroll') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="progressList.loading || progressList.data?.length"
|
||||||
|
class="max-h-[63vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<ListView
|
||||||
|
:columns="progressColumns"
|
||||||
|
:rows="progressList.data"
|
||||||
|
rowKey="name"
|
||||||
|
:options="{
|
||||||
|
selectable: false,
|
||||||
|
showTooltip: false,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ListHeader
|
||||||
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-white border-b rounded-none p-2"
|
||||||
|
>
|
||||||
|
<ListHeaderItem
|
||||||
|
:item="item"
|
||||||
|
v-for="item in progressColumns"
|
||||||
|
:key="item.key"
|
||||||
|
>
|
||||||
|
</ListHeaderItem>
|
||||||
|
</ListHeader>
|
||||||
|
<ListRows v-for="row in progressList.data" class="max-h-[500px]">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'Profile',
|
||||||
|
params: { username: row.member_username },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ListRow :row="row">
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem
|
||||||
|
:item="row[column.key]"
|
||||||
|
:align="column.align"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<div v-if="column.key == 'member_name'">
|
||||||
|
<Avatar
|
||||||
|
class="flex items-center"
|
||||||
|
:image="row['member_image']"
|
||||||
|
:label="item"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ProgressBar
|
||||||
|
v-else-if="column.key == 'progress'"
|
||||||
|
:progress="Math.ceil(row[column.key])"
|
||||||
|
class="!mx-0 !mr-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div v-if="column.key == 'creation'">
|
||||||
|
{{ dayjs(row[column.key]).format('DD MMM YYYY') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="column.key == 'progress'"
|
||||||
|
class="text-xs !mx-0 w-5"
|
||||||
|
>
|
||||||
|
{{ Math.ceil(row[column.key]) }}%
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ row[column.key].toString() }}
|
||||||
|
</div>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
|
</router-link>
|
||||||
|
</ListRows>
|
||||||
|
</ListView>
|
||||||
|
<div
|
||||||
|
v-if="progressList.data && progressList.hasNextPage"
|
||||||
|
class="flex justify-center my-3"
|
||||||
|
>
|
||||||
|
<Button @click="progressList.next()">
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div
|
||||||
|
v-if="chartDetails.data?.average_progress > 0"
|
||||||
|
class="border rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div class="text-ink-gray-5 mb-4">
|
||||||
|
{{ __('Progress Summary') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-[2fr_1fr] items-center justify-between">
|
||||||
|
<div class="flex flex-col space-y-4 flex-1 text-sm">
|
||||||
|
<div
|
||||||
|
class="flex items-center"
|
||||||
|
v-for="row in chartDetails.data?.progress_distribution"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="size-2 rounded"
|
||||||
|
:style="{
|
||||||
|
backgroundColor:
|
||||||
|
colors[theme][
|
||||||
|
row.name.startsWith('Just')
|
||||||
|
? 'red'
|
||||||
|
: row.name.startsWith('In')
|
||||||
|
? 'amber'
|
||||||
|
: 'green'
|
||||||
|
][400],
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<Tooltip :text="row.name.split('(')[1].replace(')', '')">
|
||||||
|
<div class="ml-2">
|
||||||
|
{{ row.name.split('(')[0] }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<div class="ml-auto">
|
||||||
|
{{
|
||||||
|
Math.round((row.value / course.data?.enrollments) * 100)
|
||||||
|
}}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ECharts
|
||||||
|
class="w-40 h-20"
|
||||||
|
:options="{
|
||||||
|
color: progressColors,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['50%', '70%'],
|
||||||
|
center: ['50%', '50%'],
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
scale: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data: chartDetails.data?.progress_distribution || [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showInlineLabels: false,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="lessonProgress.data?.length"
|
||||||
|
class="border rounded-lg pt-4 px-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="text-ink-gray-5">
|
||||||
|
{{ __('Lesson Completion') }}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
:options="lessonProgressSortingOptions"
|
||||||
|
@update:modelValue="(value: string) => updateLessonProgress(value)"
|
||||||
|
:placeholder="__('Sort by')"
|
||||||
|
class="!w-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y max-h-[43vh] overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="progress in lessonProgress.data"
|
||||||
|
class="flex justify-between text-sm py-2 my-1"
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
<span class="mr-3 text-xs">
|
||||||
|
{{ progress.chapter_idx }}.{{ progress.idx }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ progress.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Tooltip :text="progress.completion_count">
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
Math.ceil(
|
||||||
|
(progress.completion_count / course.data?.enrollments) *
|
||||||
|
100
|
||||||
|
)
|
||||||
|
}}%
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CourseEnrollmentModal
|
||||||
|
v-if="showEnrollmentModal"
|
||||||
|
v-model="showEnrollmentModal"
|
||||||
|
:course="course"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
createListResource,
|
||||||
|
createResource,
|
||||||
|
dayjs,
|
||||||
|
Dropdown,
|
||||||
|
ECharts,
|
||||||
|
FormControl,
|
||||||
|
ListView,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRows,
|
||||||
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { ChevronDown, Plus, Star } from 'lucide-vue-next'
|
||||||
|
import { formatAmount } from '@/utils'
|
||||||
|
import colors from '@/utils/frappe-ui-colors.json'
|
||||||
|
import CourseEnrollmentModal from '@/pages/Courses/CourseEnrollmentModal.vue'
|
||||||
|
import NumberChartGraph from '@/components/NumberChartGraph.vue'
|
||||||
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
course: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const showEnrollmentModal = ref(false)
|
||||||
|
const searchFilter = ref<string | null>(null)
|
||||||
|
const theme = ref<'darkMode' | 'lightMode'>(
|
||||||
|
localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
|
||||||
|
)
|
||||||
|
type Filters = {
|
||||||
|
course: string | undefined
|
||||||
|
member_name?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartDetails = createResource({
|
||||||
|
url: 'lms.lms.api.get_course_progress_distribution',
|
||||||
|
makeParams() {
|
||||||
|
return {
|
||||||
|
course: props.course.data?.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressList = createListResource({
|
||||||
|
doctype: 'LMS Enrollment',
|
||||||
|
filters: {
|
||||||
|
course: props.course.data?.name,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'member',
|
||||||
|
'member_name',
|
||||||
|
'member_image',
|
||||||
|
'member_username',
|
||||||
|
'progress',
|
||||||
|
'creation',
|
||||||
|
],
|
||||||
|
pageLength: 100,
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const lessonProgress = createResource({
|
||||||
|
url: 'lms.lms.api.get_lesson_completion_stats',
|
||||||
|
params: {
|
||||||
|
course: props.course.data?.name,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateLessonProgress = (value: string) => {
|
||||||
|
if (value == 'completion_rate') {
|
||||||
|
lessonProgress.data?.sort((a: any, b: any) => {
|
||||||
|
const rateA = a.completion_count / (props.course.data?.enrollments || 1)
|
||||||
|
const rateB = b.completion_count / (props.course.data?.enrollments || 1)
|
||||||
|
return rateB - rateA
|
||||||
|
})
|
||||||
|
} else if (value == 'index') {
|
||||||
|
lessonProgress.data?.sort((a: any, b: any) => {
|
||||||
|
return a.chapter_idx - b.chapter_idx || a.idx - b.idx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([searchFilter], () => {
|
||||||
|
let filterApplied = false
|
||||||
|
let filters: Filters = {
|
||||||
|
course: props.course.data?.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchFilter.value) {
|
||||||
|
filters.member_name = ['like', `%${searchFilter.value}%`]
|
||||||
|
filterApplied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
progressList.update({
|
||||||
|
filters: filters,
|
||||||
|
})
|
||||||
|
progressList.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
const averageCompletionRate = computed(() => {
|
||||||
|
let value = Math.ceil(chartDetails.data?.average_progress) || 0
|
||||||
|
return value + '%'
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressColors = computed(() => {
|
||||||
|
let colorList = []
|
||||||
|
colorList.push(colors[theme.value]['red'][400])
|
||||||
|
colorList.push(colors[theme.value]['amber'][400])
|
||||||
|
colorList.push(colors[theme.value]['green'][400])
|
||||||
|
return colorList
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressColumns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Name'),
|
||||||
|
key: 'member_name',
|
||||||
|
width: '40%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Progress'),
|
||||||
|
key: 'progress',
|
||||||
|
width: '30%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Start Date'),
|
||||||
|
key: 'creation',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const lessonProgressSortingOptions = [
|
||||||
|
{
|
||||||
|
label: __('Lesson Index'),
|
||||||
|
value: 'index',
|
||||||
|
onClick() {
|
||||||
|
updateLessonProgress('index')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Completion Rate'),
|
||||||
|
value: 'completion_rate',
|
||||||
|
onClick() {
|
||||||
|
updateLessonProgress('completion_rate')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
167
frontend/src/pages/Courses/CourseDetail.vue
Normal file
167
frontend/src/pages/Courses/CourseDetail.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="course.data">
|
||||||
|
<header
|
||||||
|
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">
|
||||||
|
<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" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<Button variant="solid" @click="childRef.submitCourse()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<CourseOverview v-if="!isAdmin" :course="course" />
|
||||||
|
<div v-else>
|
||||||
|
<Tabs :tabs="tabs" v-model="tabIndex">
|
||||||
|
<template #tab-panel="{ tab }">
|
||||||
|
<component :is="tab.component" :course="course" ref="childRef" />
|
||||||
|
</template>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
createResource,
|
||||||
|
Breadcrumbs,
|
||||||
|
Tabs,
|
||||||
|
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 CourseOverview from '@/pages/Courses/CourseOverview.vue'
|
||||||
|
import CourseDashboard from '@/pages/Courses/CourseDashboard.vue'
|
||||||
|
import CourseForm from '@/pages/Courses/CourseForm.vue'
|
||||||
|
|
||||||
|
const { brand } = sessionStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const user = inject('$user')
|
||||||
|
const tabIndex = ref(0)
|
||||||
|
const childRef = ref(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateTabIndex()
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateTabIndex = () => {
|
||||||
|
const hash = route.hash
|
||||||
|
if (hash) {
|
||||||
|
tabs.value.forEach((tab, index) => {
|
||||||
|
if (tab.label?.toLowerCase() === hash.replace('#', '')) {
|
||||||
|
tabIndex.value = index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(tabIndex, () => {
|
||||||
|
const tab = tabs.value[tabIndex.value]
|
||||||
|
if (tab.label != route.hash.replace('#', '')) {
|
||||||
|
router.push({ ...route, hash: `#${tab.label.toLowerCase()}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const course = createResource({
|
||||||
|
url: 'lms.lms.utils.get_course_details',
|
||||||
|
cache: ['course', props.courseName],
|
||||||
|
makeParams() {
|
||||||
|
return {
|
||||||
|
course: props.courseName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = ref([
|
||||||
|
{
|
||||||
|
label: __('Overview'),
|
||||||
|
component: markRaw(CourseOverview),
|
||||||
|
icon: List,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Dashboard'),
|
||||||
|
component: markRaw(CourseDashboard),
|
||||||
|
icon: TrendingUp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Settings'),
|
||||||
|
component: markRaw(CourseForm),
|
||||||
|
icon: Settings2,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.courseName,
|
||||||
|
() => {
|
||||||
|
course.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(course, () => {
|
||||||
|
if (!isAdmin.value && !course.data?.published && !course.data?.upcoming) {
|
||||||
|
router.push({
|
||||||
|
name: 'Courses',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isInstructor = () => {
|
||||||
|
let user_is_instructor = false
|
||||||
|
course.data?.instructors.forEach((instructor) => {
|
||||||
|
if (!user_is_instructor && instructor.name == user.data?.name) {
|
||||||
|
user_is_instructor = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return user_is_instructor
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = computed(() => {
|
||||||
|
return user.data?.is_moderator || isInstructor()
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
|
||||||
|
items.push({
|
||||||
|
label: course?.data?.title,
|
||||||
|
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
usePageMeta(() => {
|
||||||
|
return {
|
||||||
|
title: course?.data?.title,
|
||||||
|
icon: brand.favicon,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.avatar-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-group .avatar {
|
||||||
|
transition: margin 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
frontend/src/pages/Courses/CourseEnrollmentModal.vue
Normal file
104
frontend/src/pages/Courses/CourseEnrollmentModal.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Enroll a Student'),
|
||||||
|
size: 'xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
:label="__('Purchased Certificate')"
|
||||||
|
v-model="purchasedCertificate"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="User"
|
||||||
|
:label="__('Student')"
|
||||||
|
placeholder=" "
|
||||||
|
v-model="student"
|
||||||
|
:required="true"
|
||||||
|
:allowCreate="true"
|
||||||
|
@create="
|
||||||
|
() => {
|
||||||
|
openSettings('Members')
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="purchasedCertificate"
|
||||||
|
doctype="LMS Payment"
|
||||||
|
:label="__('Payment')"
|
||||||
|
placeholder=" "
|
||||||
|
v-model="payment"
|
||||||
|
:allowCreate="true"
|
||||||
|
@create="
|
||||||
|
() => {
|
||||||
|
openSettings('Transactions')
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ close }">
|
||||||
|
<div class="text-right">
|
||||||
|
<Button variant="solid" @click="enrollStudent(close)">
|
||||||
|
{{ __('Enroll') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, call, Dialog, FormControl, toast } from 'frappe-ui'
|
||||||
|
import { Link } from 'frappe-ui/frappe'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { openSettings } from '@/utils'
|
||||||
|
|
||||||
|
const show = defineModel<boolean>({ required: true, default: false })
|
||||||
|
const student = ref<string | null>(null)
|
||||||
|
const payment = ref<string | null>(null)
|
||||||
|
const purchasedCertificate = ref<boolean>(false)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
course: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const enrollStudent = (close: () => void) => {
|
||||||
|
let validationPassed = validateData()
|
||||||
|
if (!validationPassed) return
|
||||||
|
|
||||||
|
call('frappe.client.insert', {
|
||||||
|
doc: {
|
||||||
|
doctype: 'LMS Enrollment',
|
||||||
|
course: props.course.data?.name,
|
||||||
|
member: student.value,
|
||||||
|
payment: purchasedCertificate.value ? payment.value : null,
|
||||||
|
purchased_certificate: purchasedCertificate.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(__('Student enrolled successfully'))
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
toast.error(__(err.messages?.[0] || err))
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateData = (): boolean => {
|
||||||
|
if (!student.value) {
|
||||||
|
toast.error(__('Please select a student to enroll.'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (purchasedCertificate.value && !payment.value) {
|
||||||
|
toast.error(__('Please select a payment for the purchased certificate.'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,40 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full">
|
<div class="pl-5">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] h-full">
|
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] overflow-hidden">
|
||||||
<div>
|
<div v-if="courseResource.doc" class="h-[88vh] overflow-y-auto">
|
||||||
<header
|
<div class="my-5">
|
||||||
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
|
||||||
>
|
|
||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
|
||||||
<div class="flex items-center mt-3 md:mt-0">
|
|
||||||
<Button v-if="courseResource.data?.name" @click="trashCourse()">
|
|
||||||
<template #icon>
|
|
||||||
<Trash2 class="w-4 h-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<Button variant="solid" @click="submitCourse()" class="ml-2">
|
|
||||||
<span>
|
|
||||||
{{ __('Save') }}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="mt-5 mb-5">
|
|
||||||
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
|
|
||||||
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
|
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
|
||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.title"
|
v-model="courseResource.doc.title"
|
||||||
:label="__('Title')"
|
:label="__('Title')"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
@input="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
doctype="LMS Category"
|
doctype="LMS Category"
|
||||||
v-model="course.category"
|
v-model="courseResource.doc.category"
|
||||||
:label="__('Category')"
|
:label="__('Category')"
|
||||||
:onCreate="(value, close) => openSettings('Categories', close)"
|
:onCreate="(value, close) => openSettings('Categories', close)"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
@@ -45,6 +30,7 @@
|
|||||||
:filters="{ ignore_user_type: 1 }"
|
:filters="{ ignore_user_type: 1 }"
|
||||||
:onCreate="(close) => openSettings('Members', close)"
|
:onCreate="(close) => openSettings('Members', close)"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-ink-gray-5">
|
<div class="text-xs text-ink-gray-5">
|
||||||
@@ -60,8 +46,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center flex-wrap gap-2">
|
<div class="flex items-center flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
v-if="course.tags"
|
v-if="courseResource.doc.tags"
|
||||||
v-for="tag in course.tags?.split(', ')"
|
v-for="tag in courseResource.doc.tags?.split(', ')"
|
||||||
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md"
|
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
@@ -76,21 +62,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<Uploader
|
<Uploader
|
||||||
v-model="course.image"
|
v-model="courseResource.doc.image"
|
||||||
:label="__('Course Image')"
|
:label="__('Course Image')"
|
||||||
:required="false"
|
:required="false"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ColorSwatches
|
<ColorSwatches
|
||||||
v-model="course.card_gradient"
|
v-model="courseResource.doc.card_gradient"
|
||||||
:label="__('Color')"
|
:label="__('Color')"
|
||||||
:description="__('Choose a color for the course card')"
|
:description="__('Choose a color for the course card')"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
|
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -101,41 +89,46 @@
|
|||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.published"
|
v-model="courseResource.doc.published"
|
||||||
:label="__('Published')"
|
:label="__('Published')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.published_on"
|
v-model="courseResource.doc.published_on"
|
||||||
:label="__('Published On')"
|
:label="__('Published On')"
|
||||||
type="date"
|
type="date"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-5">
|
<div class="flex flex-col space-y-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.upcoming"
|
v-model="courseResource.doc.upcoming"
|
||||||
:label="__('Upcoming')"
|
:label="__('Upcoming')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.featured"
|
v-model="courseResource.doc.featured"
|
||||||
:label="__('Featured')"
|
:label="__('Featured')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.disable_self_learning"
|
v-model="courseResource.doc.disable_self_learning"
|
||||||
:label="__('Disable Self Enrollment')"
|
:label="__('Disable Self Enrollment')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
|
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('About the Course') }}
|
{{ __('About the Course') }}
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.short_introduction"
|
v-model="courseResource.doc.short_introduction"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="5"
|
:rows="5"
|
||||||
:label="__('Short Introduction')"
|
:label="__('Short Introduction')"
|
||||||
@@ -145,6 +138,7 @@
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
@@ -152,8 +146,13 @@
|
|||||||
<span class="text-ink-red-3">*</span>
|
<span class="text-ink-red-3">*</span>
|
||||||
</div>
|
</div>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
:content="course.description"
|
:content="courseResource.doc.description"
|
||||||
@change="(val) => (course.description = val)"
|
@change="
|
||||||
|
(val) => {
|
||||||
|
courseResource.doc.description = val
|
||||||
|
makeFormDirty()
|
||||||
|
}
|
||||||
|
"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:fixedMenu="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]"
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
@@ -161,92 +160,113 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.video_link"
|
v-model="courseResource.doc.video_link"
|
||||||
:label="__('Preview Video')"
|
:label="__('Preview Video')"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
__(
|
__(
|
||||||
'Paste the youtube link of a short video introducing the course'
|
'Paste the youtube link of a short video introducing the course'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
@input="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="related_courses"
|
v-model="related_courses"
|
||||||
doctype="LMS Course"
|
doctype="LMS Course"
|
||||||
:label="__('Related Courses')"
|
:label="__('Related Courses')"
|
||||||
:filters="{ name: ['!=', courseResource.data?.name] }"
|
:filters="{ name: ['!=', courseResource.doc?.name] }"
|
||||||
:onCreate="
|
:onCreate="
|
||||||
(close) => {
|
(close) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'CourseForm',
|
name: 'Courses',
|
||||||
params: { courseName: 'new' },
|
query: { newCourse: '1' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-5 md:px-10 pb-5 space-y-5 border-b">
|
<div class="pr-5 md:pr-10 pb-5 space-y-5 border-b">
|
||||||
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
||||||
{{ __('Pricing and Certification') }}
|
{{ __('Pricing and Certification') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.paid_course"
|
v-model="courseResource.doc.paid_course"
|
||||||
:label="__('Paid Course')"
|
:label="__('Paid Course')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.enable_certification"
|
v-model="courseResource.doc.enable_certification"
|
||||||
:label="__('Completion Certificate')"
|
:label="__('Completion Certificate')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.paid_certificate"
|
v-model="courseResource.doc.paid_certificate"
|
||||||
:label="__('Paid Certificate')"
|
:label="__('Paid Certificate')"
|
||||||
|
@change="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="course.paid_course || course.paid_certificate"
|
v-if="
|
||||||
v-model="course.course_price"
|
courseResource.doc.paid_course ||
|
||||||
|
courseResource.doc.paid_certificate
|
||||||
|
"
|
||||||
|
v-model="courseResource.doc.course_price"
|
||||||
:label="__('Amount')"
|
:label="__('Amount')"
|
||||||
:required="course.paid_course || course.paid_certificate"
|
:required="
|
||||||
|
courseResource.doc.paid_course ||
|
||||||
|
courseResource.doc.paid_certificate
|
||||||
|
"
|
||||||
|
@input="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
v-if="course.paid_certificate"
|
v-if="courseResource.doc.paid_certificate"
|
||||||
doctype="Course Evaluator"
|
doctype="Course Evaluator"
|
||||||
v-model="course.evaluator"
|
v-model="courseResource.doc.evaluator"
|
||||||
:label="__('Evaluator')"
|
:label="__('Evaluator')"
|
||||||
:required="course.paid_certificate"
|
:required="courseResource.doc.paid_certificate"
|
||||||
:onCreate="
|
:onCreate="
|
||||||
(value, close) => openSettings('Evaluators', close)
|
(value, close) => openSettings('Evaluators', close)
|
||||||
"
|
"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<Link
|
<Link
|
||||||
v-if="course.paid_course || course.paid_certificate"
|
v-if="
|
||||||
|
courseResource.doc.paid_course ||
|
||||||
|
courseResource.doc.paid_certificate
|
||||||
|
"
|
||||||
doctype="Currency"
|
doctype="Currency"
|
||||||
v-model="course.currency"
|
v-model="courseResource.doc.currency"
|
||||||
:filters="{ enabled: 1 }"
|
:filters="{ enabled: 1 }"
|
||||||
:label="__('Currency')"
|
:label="__('Currency')"
|
||||||
:required="course.paid_course || course.paid_certificate"
|
:required="
|
||||||
|
courseResource.doc.paid_course ||
|
||||||
|
courseResource.doc.paid_certificate
|
||||||
|
"
|
||||||
|
@update:modelValue="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="course.paid_certificate"
|
v-if="courseResource.doc.paid_certificate"
|
||||||
v-model="course.timezone"
|
v-model="courseResource.doc.timezone"
|
||||||
:label="__('Timezone')"
|
:label="__('Timezone')"
|
||||||
:required="course.paid_certificate"
|
:required="courseResource.doc.paid_certificate"
|
||||||
:placeholder="__('e.g. IST, UTC, GMT...')"
|
:placeholder="__('e.g. IST, UTC, GMT...')"
|
||||||
|
@input="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-5 md:px-10 pb-5 space-y-5">
|
<div class="pr-5 md:pr-10 pb-5 space-y-5">
|
||||||
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
||||||
{{ __('Meta Tags') }}
|
{{ __('Meta Tags') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -256,6 +276,7 @@
|
|||||||
:label="__('Meta Description')"
|
:label="__('Meta Description')"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="7"
|
:rows="7"
|
||||||
|
@input="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="meta.keywords"
|
v-model="meta.keywords"
|
||||||
@@ -263,16 +284,17 @@
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="7"
|
:rows="7"
|
||||||
:placeholder="__('Comma separated keywords for SEO')"
|
:placeholder="__('Comma separated keywords for SEO')"
|
||||||
|
@input="makeFormDirty()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l">
|
<div class="border-l h-[88vh] overflow-y-auto">
|
||||||
<CourseOutline
|
<CourseOutline
|
||||||
v-if="courseResource.data"
|
v-if="courseResource.doc"
|
||||||
:courseName="courseResource.data.name"
|
:courseName="courseResource.doc.name"
|
||||||
:title="__('Course Outline')"
|
:title="__('Chapters')"
|
||||||
:allowEdit="true"
|
:allowEdit="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,10 +303,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
|
||||||
TextEditor,
|
TextEditor,
|
||||||
Button,
|
Button,
|
||||||
createResource,
|
createResource,
|
||||||
|
createDocumentResource,
|
||||||
FormControl,
|
FormControl,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
@@ -293,7 +315,6 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
onMounted,
|
onMounted,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
computed,
|
|
||||||
ref,
|
ref,
|
||||||
reactive,
|
reactive,
|
||||||
watch,
|
watch,
|
||||||
@@ -308,8 +329,7 @@ import {
|
|||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { Trash2, X } from 'lucide-vue-next'
|
import { Trash2, X } from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
import { sessionStore } from '../../stores/session'
|
||||||
import { sessionStore } from '../stores/session'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
@@ -323,39 +343,15 @@ const router = useRouter()
|
|||||||
const instructors = ref([])
|
const instructors = ref([])
|
||||||
const related_courses = ref([])
|
const related_courses = ref([])
|
||||||
const app = getCurrentInstance()
|
const app = getCurrentInstance()
|
||||||
const { capture } = useTelemetry()
|
|
||||||
const { updateOnboardingStep } = useOnboarding('learning')
|
|
||||||
const { $dialog } = app.appContext.config.globalProperties
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
const isDirty = ref(false)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
course: {
|
||||||
type: String,
|
type: Object,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const course = reactive({
|
|
||||||
title: '',
|
|
||||||
short_introduction: '',
|
|
||||||
description: '',
|
|
||||||
video_link: '',
|
|
||||||
image: null,
|
|
||||||
card_gradient: '',
|
|
||||||
tags: '',
|
|
||||||
category: '',
|
|
||||||
published: false,
|
|
||||||
published_on: '',
|
|
||||||
featured: false,
|
|
||||||
upcoming: false,
|
|
||||||
disable_self_learning: false,
|
|
||||||
enable_certification: false,
|
|
||||||
paid_course: false,
|
|
||||||
paid_certificate: false,
|
|
||||||
course_price: '',
|
|
||||||
currency: '',
|
|
||||||
evaluator: '',
|
|
||||||
timezone: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const meta = reactive({
|
const meta = reactive({
|
||||||
description: '',
|
description: '',
|
||||||
keywords: '',
|
keywords: '',
|
||||||
@@ -365,18 +361,92 @@ onMounted(() => {
|
|||||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||||
router.push({ name: 'Courses' })
|
router.push({ name: 'Courses' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.courseName !== 'new') {
|
|
||||||
fetchCourseInfo()
|
|
||||||
} else {
|
|
||||||
capture('course_form_opened')
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', keyboardShortcut)
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchCourseInfo = () => {
|
const courseResource = createDocumentResource({
|
||||||
courseResource.reload()
|
doctype: 'LMS Course',
|
||||||
getMetaInfo('courses', props.courseName, meta)
|
name: props.course.data?.name,
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => courseResource.doc,
|
||||||
|
() => {
|
||||||
|
check_permission()
|
||||||
|
getMetaInfo('courses', courseResource.doc?.name, meta)
|
||||||
|
updateCourseData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateCourseData = () => {
|
||||||
|
Object.keys(courseResource.doc).forEach((key) => {
|
||||||
|
if (key == 'instructors') {
|
||||||
|
instructors.value = []
|
||||||
|
courseResource.doc.instructors.forEach((instructor) => {
|
||||||
|
instructors.value.push(instructor.instructor)
|
||||||
|
})
|
||||||
|
} else if (key == 'related_courses') {
|
||||||
|
related_courses.value = []
|
||||||
|
courseResource.doc.related_courses.forEach((course) => {
|
||||||
|
related_courses.value.push(course.course)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let checkboxes = [
|
||||||
|
'published',
|
||||||
|
'upcoming',
|
||||||
|
'disable_self_learning',
|
||||||
|
'paid_course',
|
||||||
|
'featured',
|
||||||
|
'enable_certification',
|
||||||
|
'paid_certificate',
|
||||||
|
]
|
||||||
|
for (let idx in checkboxes) {
|
||||||
|
let key = checkboxes[idx]
|
||||||
|
courseResource.doc[key] = courseResource.doc[key] ? true : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCourse = () => {
|
||||||
|
validateFields()
|
||||||
|
updateCourse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFields = () => {
|
||||||
|
courseResource.doc.description = sanitizeHTML(courseResource.doc.description)
|
||||||
|
|
||||||
|
Object.keys(courseResource.doc).forEach((key) => {
|
||||||
|
if (key != 'description' && typeof courseResource.doc[key] === 'string') {
|
||||||
|
courseResource.doc[key] = escapeHTML(courseResource.doc[key])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCourse = () => {
|
||||||
|
courseResource.setValue.submit(
|
||||||
|
{
|
||||||
|
...courseResource.doc,
|
||||||
|
instructors: instructors.value.map((instructor) => ({
|
||||||
|
instructor: instructor,
|
||||||
|
})),
|
||||||
|
related_courses: related_courses.value.map((course) => ({
|
||||||
|
course: course,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
updateMetaInfo('courses', courseResource.doc?.name, meta)
|
||||||
|
toast.success(__('Course updated successfully'))
|
||||||
|
isDirty.value = false
|
||||||
|
courseResource.reload()
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboardShortcut = (e) => {
|
const keyboardShortcut = (e) => {
|
||||||
@@ -394,151 +464,11 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('keydown', keyboardShortcut)
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
const courseCreationResource = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'LMS Course',
|
|
||||||
image: course.image,
|
|
||||||
instructors: instructors.value.map((instructor) => ({
|
|
||||||
instructor: instructor,
|
|
||||||
})),
|
|
||||||
related_courses: related_courses.value.map((course) => ({
|
|
||||||
course: course,
|
|
||||||
})),
|
|
||||||
...values,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const courseEditResource = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
auto: false,
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Course',
|
|
||||||
name: values.course,
|
|
||||||
fieldname: {
|
|
||||||
image: course.image,
|
|
||||||
instructors: instructors.value.map((instructor) => ({
|
|
||||||
instructor: instructor,
|
|
||||||
})),
|
|
||||||
related_courses: related_courses.value.map((course) => ({
|
|
||||||
course: course,
|
|
||||||
})),
|
|
||||||
...course,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const courseResource = createResource({
|
|
||||||
url: 'frappe.client.get',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Course',
|
|
||||||
name: props.courseName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: false,
|
|
||||||
onSuccess(data) {
|
|
||||||
Object.keys(data).forEach((key) => {
|
|
||||||
if (key == 'instructors') {
|
|
||||||
instructors.value = []
|
|
||||||
data.instructors.forEach((instructor) => {
|
|
||||||
instructors.value.push(instructor.instructor)
|
|
||||||
})
|
|
||||||
} else if (key == 'related_courses') {
|
|
||||||
related_courses.value = []
|
|
||||||
data.related_courses.forEach((course) => {
|
|
||||||
related_courses.value.push(course.course)
|
|
||||||
})
|
|
||||||
} else if (Object.hasOwn(course, key)) course[key] = data[key]
|
|
||||||
})
|
|
||||||
let checkboxes = [
|
|
||||||
'published',
|
|
||||||
'upcoming',
|
|
||||||
'disable_self_learning',
|
|
||||||
'paid_course',
|
|
||||||
'featured',
|
|
||||||
'enable_certification',
|
|
||||||
'paid_certificate',
|
|
||||||
]
|
|
||||||
for (let idx in checkboxes) {
|
|
||||||
let key = checkboxes[idx]
|
|
||||||
course[key] = course[key] ? true : false
|
|
||||||
}
|
|
||||||
|
|
||||||
check_permission()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateFields = () => {
|
|
||||||
course.description = sanitizeHTML(course.description)
|
|
||||||
|
|
||||||
Object.keys(course).forEach((key) => {
|
|
||||||
if (key != 'description' && typeof course[key] === 'string') {
|
|
||||||
course[key] = escapeHTML(course[key])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitCourse = () => {
|
|
||||||
validateFields()
|
|
||||||
if (courseResource.data) {
|
|
||||||
editCourse()
|
|
||||||
} else {
|
|
||||||
createCourse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCourse = () => {
|
|
||||||
courseCreationResource.submit(course, {
|
|
||||||
onSuccess(data) {
|
|
||||||
updateMetaInfo('courses', data.name, meta)
|
|
||||||
if (user.data?.is_system_manager) {
|
|
||||||
updateOnboardingStep('create_first_course', true, false, () => {
|
|
||||||
localStorage.setItem('firstCourse', data.name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
capture('course_created')
|
|
||||||
toast.success(__('Course created successfully'))
|
|
||||||
router.push({
|
|
||||||
name: 'CourseForm',
|
|
||||||
params: { courseName: data.name },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
toast.error(err.messages?.[0] || err)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const editCourse = () => {
|
|
||||||
courseEditResource.submit(
|
|
||||||
{
|
|
||||||
course: courseResource.data.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
updateMetaInfo('courses', props.courseName, meta)
|
|
||||||
toast.success(__('Course updated successfully'))
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
toast.error(err.messages?.[0] || err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteCourse = createResource({
|
const deleteCourse = createResource({
|
||||||
url: 'lms.lms.api.delete_course',
|
url: 'lms.lms.api.delete_course',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
course: props.courseName,
|
course: courseResource.doc?.name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
@@ -567,28 +497,23 @@ const trashCourse = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.courseName !== 'new',
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
fetchCourseInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateTags = () => {
|
const updateTags = () => {
|
||||||
if (newTag.value) {
|
if (newTag.value) {
|
||||||
course.tags = course.tags ? `${course.tags}, ${newTag.value}` : newTag.value
|
courseResource.doc.tags = courseResource.doc.tags
|
||||||
|
? `${courseResource.doc.tags}, ${newTag.value}`
|
||||||
|
: newTag.value
|
||||||
newTag.value = ''
|
newTag.value = ''
|
||||||
|
makeFormDirty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeTag = (tag) => {
|
const removeTag = (tag) => {
|
||||||
course.tags = course.tags
|
courseResource.doc.tags = courseResource.doc.tags
|
||||||
?.split(', ')
|
?.split(', ')
|
||||||
.filter((t) => t !== tag)
|
.filter((t) => t !== tag)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
newTag.value = ''
|
newTag.value = ''
|
||||||
|
makeFormDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
const check_permission = () => {
|
const check_permission = () => {
|
||||||
@@ -606,30 +531,20 @@ const check_permission = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const makeFormDirty = () => {
|
||||||
let crumbs = [
|
isDirty.value = true
|
||||||
{
|
}
|
||||||
label: 'Courses',
|
|
||||||
route: { name: 'Courses' },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if (courseResource.data) {
|
|
||||||
crumbs.push({
|
|
||||||
label: course.title,
|
|
||||||
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
crumbs.push({
|
|
||||||
label: props.courseName == 'new' ? 'New Course' : 'Edit Course',
|
|
||||||
route: { name: 'CourseForm', params: { courseName: props.courseName } },
|
|
||||||
})
|
|
||||||
return crumbs
|
|
||||||
})
|
|
||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
return {
|
return {
|
||||||
title: courseResource.data?.title || __('New Course'),
|
title: courseResource.doc?.title,
|
||||||
icon: brand.favicon,
|
icon: brand.favicon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
submitCourse,
|
||||||
|
trashCourse,
|
||||||
|
isDirty,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
102
frontend/src/pages/Courses/CourseOverview.vue
Normal file
102
frontend/src/pages/Courses/CourseOverview.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex justify-between w-full space-x-5">
|
||||||
|
<div class="md:w-2/3">
|
||||||
|
<div class="text-3xl font-semibold text-ink-gray-9">
|
||||||
|
{{ course.data.title }}
|
||||||
|
</div>
|
||||||
|
<div class="my-3 leading-6 text-ink-gray-7">
|
||||||
|
{{ course.data.short_introduction }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Tooltip
|
||||||
|
v-if="parseInt(course.data.rating) > 0"
|
||||||
|
:text="__('Average Rating')"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<Star class="size-4 text-transparent fill-yellow-500" />
|
||||||
|
<span class="ml-1 text-ink-gray-7">
|
||||||
|
{{ course.data.rating }}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<span v-if="parseInt(course.data.rating) > 0" class="mx-3"
|
||||||
|
>·</span
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
v-if="course.data.enrollment_count"
|
||||||
|
:text="__('Enrolled Students')"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<Users class="h-4 w-4 text-ink-gray-7" />
|
||||||
|
<span class="ml-1">
|
||||||
|
{{ course.data.enrollment_count_formatted }}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<span v-if="course.data.enrollment_count" class="mx-3">·</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="h-6 mr-1"
|
||||||
|
:class="{
|
||||||
|
'avatar-group overlap': course.data.instructors.length > 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
v-for="instructor in course.data.instructors"
|
||||||
|
:user="instructor"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<CourseInstructors :instructors="course.data.instructors" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="course.data.tags" class="flex my-4 w-fit">
|
||||||
|
<Badge
|
||||||
|
theme="gray"
|
||||||
|
size="lg"
|
||||||
|
class="mr-2 text-ink-gray-9"
|
||||||
|
v-for="tag in course.data.tags.split(', ')"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="md:hidden my-4">
|
||||||
|
<CourseCardOverlay :course="course" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-html="course.data.description"
|
||||||
|
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
|
||||||
|
></div>
|
||||||
|
<div class="mt-10">
|
||||||
|
<CourseOutline
|
||||||
|
:title="__('Course Outline')"
|
||||||
|
:courseName="course.data.name"
|
||||||
|
:showOutline="true"
|
||||||
|
:getProgress="course.data.membership ? true : false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CourseReviews
|
||||||
|
:courseName="course.data.name"
|
||||||
|
:avg_rating="course.data.rating"
|
||||||
|
:membership="course.data.membership"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<CourseCardOverlay :course="course" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<RelatedCourses :courseName="course.data.name" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Star, Users } from 'lucide-vue-next'
|
||||||
|
import { Badge, Tooltip } from 'frappe-ui'
|
||||||
|
import CourseCardOverlay from '@/components/CourseCardOverlay.vue'
|
||||||
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
|
import CourseReviews from '@/components/CourseReviews.vue'
|
||||||
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
|
import RelatedCourses from '@/components/RelatedCourses.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
course: any
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placement="start"
|
placement="right"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
v-if="canCreateCourse()"
|
v-if="canCreateCourse()"
|
||||||
:options="[
|
:options="[
|
||||||
@@ -13,10 +13,7 @@
|
|||||||
label: __('New Course'),
|
label: __('New Course'),
|
||||||
icon: 'book-open',
|
icon: 'book-open',
|
||||||
onClick() {
|
onClick() {
|
||||||
router.push({
|
showCourseModal = true
|
||||||
name: 'CourseForm',
|
|
||||||
params: { courseName: 'new' },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,6 +106,11 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NewCourseModal
|
||||||
|
v-if="showCourseModal"
|
||||||
|
v-model="showCourseModal"
|
||||||
|
:courses="courses"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
@@ -128,13 +130,19 @@ import { sessionStore } from '@/stores/session'
|
|||||||
import { canCreateCourse } from '@/utils'
|
import { canCreateCourse } from '@/utils'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
import router from '../router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import NewCourseModal from '@/pages/Courses/NewCourseModal.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const start = ref(0)
|
const start = ref(0)
|
||||||
const pageLength = ref(30)
|
const pageLength = ref(30)
|
||||||
const categories = ref([])
|
const categories = ref([
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
])
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const certification = ref(false)
|
const certification = ref(false)
|
||||||
@@ -142,17 +150,13 @@ const filters = ref({})
|
|||||||
const currentTab = ref('Live')
|
const currentTab = ref('Live')
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const courseCount = ref(0)
|
const courseCount = ref(0)
|
||||||
|
const router = useRouter()
|
||||||
|
const showCourseModal = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setFiltersFromQuery()
|
setFiltersFromQuery()
|
||||||
updateCourses()
|
updateCourses()
|
||||||
getCourseCount()
|
getCourseCount()
|
||||||
categories.value = [
|
|
||||||
{
|
|
||||||
label: '',
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const setFiltersFromQuery = () => {
|
const setFiltersFromQuery = () => {
|
||||||
@@ -160,6 +164,9 @@ const setFiltersFromQuery = () => {
|
|||||||
title.value = queries.get('title') || ''
|
title.value = queries.get('title') || ''
|
||||||
currentCategory.value = queries.get('category') || null
|
currentCategory.value = queries.get('category') || null
|
||||||
certification.value = queries.get('certification') || false
|
certification.value = queries.get('certification') || false
|
||||||
|
if (queries.get('newCourse') == '1') {
|
||||||
|
showCourseModal.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const courses = createListResource({
|
const courses = createListResource({
|
||||||
@@ -168,9 +175,6 @@ const courses = createListResource({
|
|||||||
cache: ['courses', user.data?.name],
|
cache: ['courses', user.data?.name],
|
||||||
pageLength: pageLength.value,
|
pageLength: pageLength.value,
|
||||||
start: start.value,
|
start: start.value,
|
||||||
onSuccess(data) {
|
|
||||||
setCategories(data)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const setCategories = (data) => {
|
const setCategories = (data) => {
|
||||||
@@ -205,7 +209,7 @@ const identifyUserPersona = async () => {
|
|||||||
|
|
||||||
const getCourseCount = () => {
|
const getCourseCount = () => {
|
||||||
if (!user.data) return
|
if (!user.data) return
|
||||||
|
if (!user.data.is_moderator) return
|
||||||
call('frappe.client.get_count', {
|
call('frappe.client.get_count', {
|
||||||
doctype: 'LMS Course',
|
doctype: 'LMS Course',
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
@@ -219,7 +223,9 @@ const updateCourses = () => {
|
|||||||
courses.update({
|
courses.update({
|
||||||
filters: filters.value,
|
filters: filters.value,
|
||||||
})
|
})
|
||||||
courses.reload()
|
courses.reload().then((data) => {
|
||||||
|
setCategories(data)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
156
frontend/src/pages/Courses/NewCourseModal.vue
Normal file
156
frontend/src/pages/Courses/NewCourseModal.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Create Course'),
|
||||||
|
size: '3xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="text-base">
|
||||||
|
<div class="grid grid-cols-2 gap-5 border-b mb-5">
|
||||||
|
<FormControl
|
||||||
|
v-model="course.title"
|
||||||
|
:label="__('Title')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="LMS Category"
|
||||||
|
v-model="course.category"
|
||||||
|
:label="__('Category')"
|
||||||
|
:allowCreate="true"
|
||||||
|
@create="
|
||||||
|
() => {
|
||||||
|
openSettings('Categories')
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
v-model="course.instructors"
|
||||||
|
doctype="User"
|
||||||
|
:label="__('Instructors')"
|
||||||
|
:filters="{ ignore_user_type: 1 }"
|
||||||
|
:onCreate="(close: () => void) => openSettings('Members', close)"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Uploader
|
||||||
|
v-model="course.image"
|
||||||
|
:label="__('Course Image')"
|
||||||
|
:required="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormControl
|
||||||
|
v-model="course.short_introduction"
|
||||||
|
:label="__('Short Introduction')"
|
||||||
|
type="textarea"
|
||||||
|
:required="true"
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
<div class="">
|
||||||
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
|
{{ __('Course Description') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
:content="course.description"
|
||||||
|
@change="(val: string) => (course.description = val)"
|
||||||
|
: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-[10rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ close }">
|
||||||
|
<div class="text-right">
|
||||||
|
<Button variant="solid" @click="saveCourse(close)">
|
||||||
|
{{ __('Create') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||||
|
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
||||||
|
import { inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { openSettings } from '@/utils'
|
||||||
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
|
import Uploader from '@/components/Controls/Uploader.vue'
|
||||||
|
|
||||||
|
const show = defineModel<boolean>({ required: true, default: false })
|
||||||
|
const router = useRouter()
|
||||||
|
const { capture } = useTelemetry()
|
||||||
|
const { updateOnboardingStep } = useOnboarding('learning')
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
courses: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const course = ref({
|
||||||
|
title: '',
|
||||||
|
short_introduction: '',
|
||||||
|
description: '',
|
||||||
|
instructors: [],
|
||||||
|
category: null,
|
||||||
|
image: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveCourse = (close: () => void = () => {}) => {
|
||||||
|
props.courses.insert.submit(
|
||||||
|
{
|
||||||
|
...course.value,
|
||||||
|
instructors: course.value.instructors.map((instructor) => ({
|
||||||
|
instructor: instructor,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data: any) {
|
||||||
|
toast.success(__('Course created successfully'))
|
||||||
|
close()
|
||||||
|
capture('course_created')
|
||||||
|
router.push({
|
||||||
|
name: 'CourseDetail',
|
||||||
|
params: { courseName: data.name },
|
||||||
|
hash: '#settings',
|
||||||
|
})
|
||||||
|
if (user.data?.is_system_manager) {
|
||||||
|
updateOnboardingStep('create_first_course', true, false, () => {
|
||||||
|
localStorage.setItem('firstCourse', data.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardShortcut = (e: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
e.key === 's' &&
|
||||||
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
e.target &&
|
||||||
|
e.target instanceof HTMLElement &&
|
||||||
|
!e.target.classList.contains('ProseMirror')
|
||||||
|
) {
|
||||||
|
saveCourse()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(show, () => {
|
||||||
|
capture('course_form_opened')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'CourseForm', params: { courseName: 'new' } }"
|
:to="{ name: 'Courses', query: { newCourse: '1' } }"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
<Uploader
|
<Uploader
|
||||||
v-model="job.company_logo"
|
v-model="job.company_logo"
|
||||||
:label="__('Company Logo')"
|
:label="__('Company Logo')"
|
||||||
:required="false"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,12 +51,12 @@
|
|||||||
class="hidden lg:block"
|
class="hidden lg:block"
|
||||||
@change="updateJobs"
|
@change="updateJobs"
|
||||||
/>
|
/>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="flex items-center space-x-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="__('Search')"
|
:placeholder="__('Search')"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
class="w-full max-w-40"
|
class="w-full"
|
||||||
@input="updateJobs"
|
@input="updateJobs"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -79,17 +79,17 @@
|
|||||||
v-model="jobType"
|
v-model="jobType"
|
||||||
type="select"
|
type="select"
|
||||||
:options="jobTypes"
|
:options="jobTypes"
|
||||||
class="w-full"
|
class="w-full min-w-32"
|
||||||
:placeholder="__('Type')"
|
:placeholder="__('Type')"
|
||||||
@change="updateJobs"
|
@update:modelValue="updateJobs"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="workMode"
|
v-model="workMode"
|
||||||
type="select"
|
type="select"
|
||||||
:options="workModes"
|
:options="workModes"
|
||||||
class="w-full"
|
class="w-full min-w-32"
|
||||||
:placeholder="__('Work Mode')"
|
:placeholder="__('Work Mode')"
|
||||||
@change="updateJobs"
|
@update:modelValue="updateJobs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,13 +218,13 @@ const updateJobs = () => {
|
|||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
filters.value.status = 'Open'
|
filters.value.status = 'Open'
|
||||||
|
|
||||||
if (jobType.value) {
|
if (jobType.value && jobType.value !== ' ') {
|
||||||
filters.value.type = jobType.value
|
filters.value.type = jobType.value
|
||||||
} else {
|
} else {
|
||||||
delete filters.value.type
|
delete filters.value.type
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workMode.value) {
|
if (workMode.value && workMode.value !== ' ') {
|
||||||
filters.value.work_mode = workMode.value
|
filters.value.work_mode = workMode.value
|
||||||
} else {
|
} else {
|
||||||
delete filters.value.work_mode
|
delete filters.value.work_mode
|
||||||
@@ -271,7 +271,7 @@ watch(jobs, () => {
|
|||||||
|
|
||||||
const jobTypes = computed(() => {
|
const jobTypes = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: '', value: '' },
|
{ label: ' ', value: ' ' },
|
||||||
{ label: __('Full Time'), value: 'Full Time' },
|
{ label: __('Full Time'), value: 'Full Time' },
|
||||||
{ label: __('Part Time'), value: 'Part Time' },
|
{ label: __('Part Time'), value: 'Part Time' },
|
||||||
{ label: __('Contract'), value: 'Contract' },
|
{ label: __('Contract'), value: 'Contract' },
|
||||||
@@ -281,7 +281,7 @@ const jobTypes = computed(() => {
|
|||||||
|
|
||||||
const workModes = computed(() => {
|
const workModes = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: '', value: '' },
|
{ label: ' ', value: ' ' },
|
||||||
{ label: 'On site', value: 'On-site' },
|
{ label: 'On site', value: 'On-site' },
|
||||||
{ label: 'Hybrid', value: 'Hybrid' },
|
{ label: 'Hybrid', value: 'Hybrid' },
|
||||||
{ label: 'Remote', value: 'Remote' },
|
{ label: 'Remote', value: 'Remote' },
|
||||||
|
|||||||
@@ -326,6 +326,7 @@
|
|||||||
@updateNotes="updateNotes"
|
@updateNotes="updateNotes"
|
||||||
/>
|
/>
|
||||||
<VideoStatistics
|
<VideoStatistics
|
||||||
|
v-if="showStatsDialog"
|
||||||
v-model="showStatsDialog"
|
v-model="showStatsDialog"
|
||||||
:lessonName="lesson.data?.name"
|
:lessonName="lesson.data?.name"
|
||||||
:lessonTitle="lesson.data?.title"
|
:lessonTitle="lesson.data?.title"
|
||||||
@@ -871,6 +872,7 @@ const scrollDiscussionsIntoView = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateNotes = () => {
|
const updateNotes = () => {
|
||||||
|
if (!user.data) return
|
||||||
notes.update({
|
notes.update({
|
||||||
filters: {
|
filters: {
|
||||||
lesson: lesson.data?.name,
|
lesson: lesson.data?.name,
|
||||||
|
|||||||
@@ -471,7 +471,11 @@ const breadcrumbs = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: lessonDetails.data?.course_title,
|
label: lessonDetails.data?.course_title,
|
||||||
route: { name: 'CourseForm', params: { courseName: props.courseName } },
|
route: {
|
||||||
|
name: 'CourseDetail',
|
||||||
|
params: { courseName: props.courseName },
|
||||||
|
hash: '#settings',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
class="flex space-x-2 px-2 py-4"
|
class="flex space-x-2 px-2 py-4"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-pointer': log.link,
|
'cursor-pointer': log.link,
|
||||||
'items-center': !showDetails(log) && !isMention(log),
|
'items-center': !showDetails(log) && !isMentionOrComment(log),
|
||||||
}"
|
}"
|
||||||
@click="navigateToPage(log)"
|
@click="navigateToPage(log)"
|
||||||
>
|
>
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isMention(log)"
|
v-if="isMentionOrComment(log)"
|
||||||
v-html="log.email_content"
|
v-html="log.email_content"
|
||||||
class="bg-surface-gray-2 rounded-md px-3 py-2"
|
class="bg-surface-gray-2 rounded-md px-3 py-2 line-clamp-3 overflow-hidden"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-else-if="showDetails(log)"
|
v-else-if="showDetails(log)"
|
||||||
@@ -185,10 +185,9 @@ const unReadNotifications = createListResource({
|
|||||||
doctype: 'Notification Log',
|
doctype: 'Notification Log',
|
||||||
url: 'lms.lms.api.get_notifications',
|
url: 'lms.lms.api.get_notifications',
|
||||||
filters: {
|
filters: {
|
||||||
for_user: user.data?.name,
|
|
||||||
read: 0,
|
read: 0,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: user.data ? true : false,
|
||||||
cache: 'Unread Notifications',
|
cache: 'Unread Notifications',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -196,18 +195,17 @@ const readNotifications = createListResource({
|
|||||||
doctype: 'Notification Log',
|
doctype: 'Notification Log',
|
||||||
url: 'lms.lms.api.get_notifications',
|
url: 'lms.lms.api.get_notifications',
|
||||||
filters: {
|
filters: {
|
||||||
for_user: user.data?.name,
|
|
||||||
read: 1,
|
read: 1,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: user.data ? true : false,
|
||||||
cache: 'Read Notifications',
|
cache: 'Read Notifications',
|
||||||
})
|
})
|
||||||
|
|
||||||
const markAsRead = createResource({
|
const markAsRead = createResource({
|
||||||
url: 'lms.lms.api.mark_as_read',
|
url: 'frappe.desk.doctype.notification_log.notification_log.mark_as_read',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
name: values.name,
|
docname: values.name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
@@ -217,7 +215,7 @@ const markAsRead = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const markAllAsRead = createResource({
|
const markAllAsRead = createResource({
|
||||||
url: 'lms.lms.api.mark_all_as_read',
|
url: 'frappe.desk.doctype.notification_log.notification_log.mark_all_as_read',
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
unReadNotifications.reload()
|
unReadNotifications.reload()
|
||||||
readNotifications.reload()
|
readNotifications.reload()
|
||||||
@@ -260,7 +258,7 @@ const navigateToPage = (log) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMention = (log) => {
|
const isMentionOrComment = (log) => {
|
||||||
if (log.type == 'Mention') {
|
if (log.type == 'Mention') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="profile.data.open_to"
|
v-if="profile.data.open_to"
|
||||||
:text="
|
:text="
|
||||||
profile.data.open_to === 'Opportunities'
|
profile.data.open_to === 'Work'
|
||||||
? __('Open to Opportunities')
|
? __('Open to Work')
|
||||||
: __('Hiring')
|
: __('Hiring')
|
||||||
"
|
"
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<div
|
<div
|
||||||
class="rounded-full w-fit"
|
class="rounded-full w-fit"
|
||||||
:class="
|
:class="
|
||||||
profile.data.open_to === 'Opportunities'
|
profile.data.open_to === 'Work'
|
||||||
? 'bg-surface-green-3'
|
? 'bg-surface-green-3'
|
||||||
: 'bg-purple-500'
|
: 'bg-purple-500'
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ import {
|
|||||||
onMounted,
|
onMounted,
|
||||||
inject,
|
inject,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
watch,
|
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import { ClipboardList, ListChecks, Plus, Trash2 } from 'lucide-vue-next'
|
import { ClipboardList, ListChecks, Plus, Trash2 } from 'lucide-vue-next'
|
||||||
@@ -252,7 +251,9 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const questions = ref([])
|
const questions = computed(() => {
|
||||||
|
return quizDetails.doc?.questions || []
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||||
@@ -273,24 +274,10 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('keydown', keyboardShortcut)
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.quizID !== 'new',
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
quizDetails.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const quizDetails = createDocumentResource({
|
const quizDetails = createDocumentResource({
|
||||||
doctype: 'LMS Quiz',
|
doctype: 'LMS Quiz',
|
||||||
name: props.quizID,
|
name: props.quizID,
|
||||||
auto: false,
|
auto: false,
|
||||||
onSuccess(doc) {
|
|
||||||
if (doc.questions && doc.questions.length > 0) {
|
|
||||||
questions.value = doc.questions.map((question) => question)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const validateTitle = () => {
|
const validateTitle = () => {
|
||||||
|
|||||||
@@ -116,20 +116,30 @@ const debouncedSaveProgress = (scormDetails) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveDataToLMS = (key, value) => {
|
const saveDataToLMS = (key, value) => {
|
||||||
if (key === 'cmi.core.lesson_status') {
|
const isLessonStatus = key === 'cmi.core.lesson_status' && value === 'passed'
|
||||||
if (value === 'passed') {
|
const isCompletionStatus =
|
||||||
isSuccessfullyCompleted.value = true
|
key === 'cmi.completion_status' && value === 'completed'
|
||||||
saveProgress({
|
const shouldRestart =
|
||||||
is_complete: isSuccessfullyCompleted.value,
|
(key === 'cmi.core.lesson_status' && value === 'failed') ||
|
||||||
scorm_content: '',
|
(key === 'cmi.completion_status' && value === 'incomplete')
|
||||||
})
|
|
||||||
} else if (value === 'failed' && courseRestartOnFailure) {
|
if (isLessonStatus || isCompletionStatus) {
|
||||||
saveProgress({
|
isSuccessfullyCompleted.value = true
|
||||||
is_complete: isSuccessfullyCompleted.value,
|
}
|
||||||
scorm_content: '',
|
|
||||||
})
|
if (
|
||||||
}
|
isLessonStatus ||
|
||||||
} else if (key === 'cmi.suspend_data' && !isSuccessfullyCompleted.value) {
|
isCompletionStatus ||
|
||||||
|
(shouldRestart && courseRestartOnFailure)
|
||||||
|
) {
|
||||||
|
saveProgress({
|
||||||
|
is_complete: isSuccessfullyCompleted.value,
|
||||||
|
scorm_content: '',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'cmi.suspend_data' && !isSuccessfullyCompleted.value) {
|
||||||
debouncedSaveProgress({
|
debouncedSaveProgress({
|
||||||
is_complete: false,
|
is_complete: false,
|
||||||
scorm_content: value,
|
scorm_content: value,
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/courses',
|
path: '/courses',
|
||||||
name: 'Courses',
|
name: 'Courses',
|
||||||
component: () => import('@/pages/Courses.vue'),
|
component: () => import('@/pages/Courses/Courses.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/courses/:courseName',
|
path: '/courses/:courseName',
|
||||||
name: 'CourseDetail',
|
name: 'CourseDetail',
|
||||||
component: () => import('@/pages/CourseDetail.vue'),
|
component: () => import('@/pages/Courses/CourseDetail.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -30,7 +30,7 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/courses/:courseName/certification',
|
path: '/courses/:courseName/certification',
|
||||||
name: 'CourseCertification',
|
name: 'CourseCertification',
|
||||||
component: () => import('@/pages/CourseCertification.vue'),
|
component: () => import('@/pages/Courses/CourseCertification.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -119,12 +119,6 @@ const routes = [
|
|||||||
component: () => import('@/pages/JobApplications.vue'),
|
component: () => import('@/pages/JobApplications.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/courses/:courseName/edit',
|
|
||||||
name: 'CourseForm',
|
|
||||||
component: () => import('@/pages/CourseForm.vue'),
|
|
||||||
props: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber/edit',
|
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber/edit',
|
||||||
name: 'LessonForm',
|
name: 'LessonForm',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const sessionStore = defineStore('lms-session', () => {
|
|||||||
let _sessionUser = cookies.get('user_id')
|
let _sessionUser = cookies.get('user_id')
|
||||||
if (_sessionUser === 'Guest') {
|
if (_sessionUser === 'Guest') {
|
||||||
_sessionUser = null
|
_sessionUser = null
|
||||||
|
} else {
|
||||||
|
userResource.reload()
|
||||||
}
|
}
|
||||||
return _sessionUser
|
return _sessionUser
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const usersStore = defineStore('lms-users', () => {
|
|||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
auto: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const allUsers = createResource({
|
const allUsers = createResource({
|
||||||
|
|||||||
@@ -465,7 +465,6 @@ const getSidebarItems = () => {
|
|||||||
'Courses',
|
'Courses',
|
||||||
'CourseDetail',
|
'CourseDetail',
|
||||||
'Lesson',
|
'Lesson',
|
||||||
'CourseForm',
|
|
||||||
'LessonForm',
|
'LessonForm',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -490,6 +489,9 @@ const getSidebarItems = () => {
|
|||||||
icon: 'GraduationCap',
|
icon: 'GraduationCap',
|
||||||
to: 'CertifiedParticipants',
|
to: 'CertifiedParticipants',
|
||||||
activeFor: ['CertifiedParticipants'],
|
activeFor: ['CertifiedParticipants'],
|
||||||
|
condition: () => {
|
||||||
|
return userResource?.data
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export class Markdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get pasteConfig() {
|
static get pasteConfig() {
|
||||||
return { tags: ['P'] }
|
return {
|
||||||
|
tags: ['P'],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -52,11 +54,277 @@ export class Markdown {
|
|||||||
this._togglePlaceholder()
|
this._togglePlaceholder()
|
||||||
)
|
)
|
||||||
this.wrapper.addEventListener('keydown', (e) => this._onKeyDown(e))
|
this.wrapper.addEventListener('keydown', (e) => this._onKeyDown(e))
|
||||||
|
this.wrapper.addEventListener(
|
||||||
|
'paste',
|
||||||
|
(e) => this._onNativePaste(e),
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.wrapper
|
return this.wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onNativePaste(event) {
|
||||||
|
const clipboardData = event.clipboardData || window.clipboardData
|
||||||
|
if (!clipboardData) return
|
||||||
|
|
||||||
|
const pastedText = clipboardData.getData('text/plain')
|
||||||
|
|
||||||
|
if (pastedText && this._looksLikeMarkdown(pastedText)) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
|
this._insertMarkdownAsBlocks(pastedText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_looksLikeMarkdown(text) {
|
||||||
|
const markdownPatterns = [
|
||||||
|
/^#{1,6}\s+/m,
|
||||||
|
/^[\-\*]\s+/m,
|
||||||
|
/^\d+\.\s+/m,
|
||||||
|
/```[\s\S]*```/,
|
||||||
|
]
|
||||||
|
|
||||||
|
return markdownPatterns.some((pattern) => pattern.test(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
async _insertMarkdownAsBlocks(markdown) {
|
||||||
|
const blocks = this._parseMarkdownToBlocks(markdown)
|
||||||
|
|
||||||
|
if (blocks.length === 0) return
|
||||||
|
|
||||||
|
const currentIndex = this.api.blocks.getCurrentBlockIndex()
|
||||||
|
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
try {
|
||||||
|
await this.api.blocks.insert(
|
||||||
|
blocks[i].type,
|
||||||
|
blocks[i].data,
|
||||||
|
{},
|
||||||
|
currentIndex + i,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to insert block:', blocks[i], error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.blocks.delete(currentIndex + blocks.length)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete original block:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.api.caret.setToBlock(currentIndex, 'end')
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseMarkdownToBlocks(markdown) {
|
||||||
|
const lines = markdown.split('\n')
|
||||||
|
const blocks = []
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i]
|
||||||
|
|
||||||
|
if (line.trim() === '') {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.trim().startsWith('```')) {
|
||||||
|
const codeBlock = this._parseCodeBlock(lines, i)
|
||||||
|
blocks.push(codeBlock.block)
|
||||||
|
i = codeBlock.nextIndex
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^#{1,6}\s+/.test(line)) {
|
||||||
|
blocks.push(this._parseHeading(line))
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^[\s]*[-*+]\s+/.test(line)) {
|
||||||
|
const listBlock = this._parseUnorderedList(lines, i)
|
||||||
|
blocks.push(listBlock.block)
|
||||||
|
i = listBlock.nextIndex
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^[\s]*(\d+)\.\s+/.test(line)) {
|
||||||
|
const listBlock = this._parseOrderedList(lines, i)
|
||||||
|
blocks.push(listBlock.block)
|
||||||
|
i = listBlock.nextIndex
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
type: 'paragraph',
|
||||||
|
data: { text: this._parseInlineMarkdown(line) },
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseHeading(line) {
|
||||||
|
const match = line.match(/^(#{1,6})\s+(.*)$/)
|
||||||
|
const level = match[1].length
|
||||||
|
const text = match[2]
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'header',
|
||||||
|
data: {
|
||||||
|
text: this._parseInlineMarkdown(text),
|
||||||
|
level: level,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseUnorderedList(lines, startIndex) {
|
||||||
|
const items = []
|
||||||
|
let i = startIndex
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i]
|
||||||
|
|
||||||
|
if (/^[\s]*[-*+]\s+/.test(line)) {
|
||||||
|
const text = line.replace(/^[\s]*[-*+]\s+/, '')
|
||||||
|
items.push({
|
||||||
|
content: this._parseInlineMarkdown(text),
|
||||||
|
items: [],
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
} else if (line.trim() === '') {
|
||||||
|
i++
|
||||||
|
if (i < lines.length && /^[\s]*[-*+]\s+/.test(lines[i])) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
block: {
|
||||||
|
type: 'list',
|
||||||
|
data: {
|
||||||
|
style: 'unordered',
|
||||||
|
items: items,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextIndex: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseOrderedList(lines, startIndex) {
|
||||||
|
const items = []
|
||||||
|
let i = startIndex
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i]
|
||||||
|
|
||||||
|
const match = line.match(/^[\s]*(\d+)\.\s+(.*)$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const number = match[1]
|
||||||
|
const text = match[2]
|
||||||
|
|
||||||
|
if (number === '1') {
|
||||||
|
if (items.length > 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
content: this._parseInlineMarkdown(text),
|
||||||
|
items: [],
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
} else if (line.trim() === '') {
|
||||||
|
i++
|
||||||
|
if (i < lines.length && /^[\s]*(\d+)\.\s+/.test(lines[i])) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
block: {
|
||||||
|
type: 'list',
|
||||||
|
data: {
|
||||||
|
style: 'ordered',
|
||||||
|
items: items,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextIndex: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseCodeBlock(lines, startIndex) {
|
||||||
|
let i = startIndex + 1
|
||||||
|
const codeLines = []
|
||||||
|
let language = lines[startIndex].trim().substring(3).trim()
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i].trim().startsWith('```')) {
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
codeLines.push(lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
block: {
|
||||||
|
type: 'codeBox',
|
||||||
|
data: {
|
||||||
|
code: codeLines.join('\n'),
|
||||||
|
language: language || 'plaintext',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextIndex: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseInlineMarkdown(text) {
|
||||||
|
if (!text) return ''
|
||||||
|
|
||||||
|
let html = this._escapeHtml(text)
|
||||||
|
|
||||||
|
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||||
|
|
||||||
|
html = html.replace(/\*\*([^\*\n]+?)\*\*/g, '<b>$1</b>')
|
||||||
|
html = html.replace(/__([^_\n]+?)__/g, '<b>$1</b>')
|
||||||
|
|
||||||
|
html = html.replace(/\*([^\*\n]+?)\*/g, '<i>$1</i>')
|
||||||
|
html = html.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, '<i>$1</i>')
|
||||||
|
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
||||||
|
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
_escapeHtml(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
_togglePlaceholder() {
|
_togglePlaceholder() {
|
||||||
const blocks = document.querySelectorAll(
|
const blocks = document.querySelectorAll(
|
||||||
'.cdx-block.ce-paragraph[data-placeholder]'
|
'.cdx-block.ce-paragraph[data-placeholder]'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { VitePWA } from 'vite-plugin-pwa'
|
|||||||
|
|
||||||
export default defineConfig(async ({ mode }) => {
|
export default defineConfig(async ({ mode }) => {
|
||||||
const isDev = mode === 'development'
|
const isDev = mode === 'development'
|
||||||
|
console.log(mode, isDev)
|
||||||
const frappeui = await importFrappeUIPlugin(isDev)
|
const frappeui = await importFrappeUIPlugin(isDev)
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
@@ -920,16 +920,16 @@
|
|||||||
crelt "^1.0.5"
|
crelt "^1.0.5"
|
||||||
|
|
||||||
"@codemirror/state@6.x", "@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
|
"@codemirror/state@6.x", "@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
|
||||||
version "6.5.3"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.3.tgz#256e256d466f49ed0879d462031de8bd541e1403"
|
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.4.tgz#f5be4b8c0d2310180d5f15a9f641c21ca69faf19"
|
||||||
integrity sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==
|
integrity sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@marijn/find-cluster-break" "^1.0.0"
|
"@marijn/find-cluster-break" "^1.0.0"
|
||||||
|
|
||||||
"@codemirror/view@6.x", "@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.37.0":
|
"@codemirror/view@6.x", "@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.37.0":
|
||||||
version "6.39.10"
|
version "6.39.11"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.39.10.tgz#ae0dfcb635fd307aa3b800e305c9f46152503dba"
|
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.39.11.tgz#200aebef2074bfbbb7a3d5f0644c1b560d876b39"
|
||||||
integrity sha512-QfT/PXhiiP76PxMnX0RQVPDQrqfRt9wr9QhInNHnEUu4PWoNS8QwwcIDEneXFChJv22y+Yu/Cz5lFMTPz+h16w==
|
integrity sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@codemirror/state" "^6.5.0"
|
"@codemirror/state" "^6.5.0"
|
||||||
crelt "^1.0.6"
|
crelt "^1.0.6"
|
||||||
@@ -1400,130 +1400,130 @@
|
|||||||
estree-walker "^1.0.1"
|
estree-walker "^1.0.1"
|
||||||
picomatch "^2.2.2"
|
picomatch "^2.2.2"
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi@4.55.1":
|
"@rollup/rollup-android-arm-eabi@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz#76e0fef6533b3ce313f969879e61e8f21f0eeb28"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz#067cfcd81f1c1bfd92aefe3ad5ef1523549d5052"
|
||||||
integrity sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==
|
integrity sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64@4.55.1":
|
"@rollup/rollup-android-arm64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz#d3cfc675a40bbdec97bda6d7fe3b3b05f0e1cd93"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz#85e39a44034d7d4e4fee2a1616f0bddb85a80517"
|
||||||
integrity sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==
|
integrity sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.55.1":
|
"@rollup/rollup-darwin-arm64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz#eb912b8f59dd47c77b3c50a78489013b1d6772b4"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz#17d92fe98f2cc277b91101eb1528b7c0b6c00c54"
|
||||||
integrity sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==
|
integrity sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64@4.55.1":
|
"@rollup/rollup-darwin-x64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz#e7d0839fdfd1276a1d34bc5ebbbd0dfd7d0b81a0"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz#89ae6c66b1451609bd1f297da9384463f628437d"
|
||||||
integrity sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==
|
integrity sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64@4.55.1":
|
"@rollup/rollup-freebsd-arm64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz#7ff8118760f7351e48fd0cd3717ff80543d6aac8"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz#cdbdb9947b26e76c188a31238c10639347413628"
|
||||||
integrity sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==
|
integrity sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64@4.55.1":
|
"@rollup/rollup-freebsd-x64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz#49d330dadbda1d4e9b86b4a3951b59928a9489a9"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz#9b1458d07b6e040be16ee36d308a2c9520f7f7cc"
|
||||||
integrity sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==
|
integrity sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.55.1":
|
"@rollup/rollup-linux-arm-gnueabihf@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz#98c5f1f8b9776b4a36e466e2a1c9ed1ba52ef1b6"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz#1d50ded7c965d5f125f5832c971ad5b287befef7"
|
||||||
integrity sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==
|
integrity sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf@4.55.1":
|
"@rollup/rollup-linux-arm-musleabihf@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz#b9acecd3672e742f70b0c8a94075c816a91ff040"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz#53597e319b7e65990d3bc2a5048097384814c179"
|
||||||
integrity sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==
|
integrity sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.55.1":
|
"@rollup/rollup-linux-arm64-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz#7a6ab06651bc29e18b09a50ed1a02bc972977c9b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz#597002909dec198ca4bdccb25f043d32db3d6283"
|
||||||
integrity sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==
|
integrity sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl@4.55.1":
|
"@rollup/rollup-linux-arm64-musl@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz#3c8c9072ba4a4d4ef1156b85ab9a2cbb57c1fad0"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz#286f0e0f799545ce288bdc5a7c777261fcba3d54"
|
||||||
integrity sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==
|
integrity sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu@4.55.1":
|
"@rollup/rollup-linux-loong64-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz#17a7af13530f4e4a7b12cd26276c54307a84a8b0"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz#1fab07fa1a4f8d3697735b996517f1bae0ba101b"
|
||||||
integrity sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==
|
integrity sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl@4.55.1":
|
"@rollup/rollup-linux-loong64-musl@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz#5cd7a900fd7b077ecd753e34a9b7ff1157fe70c1"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz#efc2cb143d6c067f95205482afb177f78ed9ea3d"
|
||||||
integrity sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==
|
integrity sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu@4.55.1":
|
"@rollup/rollup-linux-ppc64-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz#03a097e70243ddf1c07b59d3c20f38e6f6800539"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz#e8de8bd3463f96b92b7dfb7f151fd80ffe8a937c"
|
||||||
integrity sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==
|
integrity sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl@4.55.1":
|
"@rollup/rollup-linux-ppc64-musl@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz#a5389873039d4650f35b4fa060d286392eb21a94"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz#8c508fe28a239da83b3a9da75bcf093186e064b4"
|
||||||
integrity sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==
|
integrity sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.55.1":
|
"@rollup/rollup-linux-riscv64-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz#789e60e7d6e2b76132d001ffb24ba80007fb17d0"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz#ff6d51976e0830732880770a9e18553136b8d92b"
|
||||||
integrity sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==
|
integrity sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl@4.55.1":
|
"@rollup/rollup-linux-riscv64-musl@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz#3556fa88d139282e9a73c337c9a170f3c5fe7aa4"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz#325fb35eefc7e81d75478318f0deee1e4a111493"
|
||||||
integrity sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==
|
integrity sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.55.1":
|
"@rollup/rollup-linux-s390x-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz#c085995b10143c16747a67f1a5487512b2ff04b2"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz#37410fabb5d3ba4ad34abcfbe9ba9b6288413f30"
|
||||||
integrity sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==
|
integrity sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu@4.55.1":
|
"@rollup/rollup-linux-x64-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz#9563a5419dd2604841bad31a39ccfdd2891690fb"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz#8ef907a53b2042068fc03fcc6a641e2b02276eca"
|
||||||
integrity sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==
|
integrity sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl@4.55.1":
|
"@rollup/rollup-linux-x64-musl@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz#691bb06e6269a8959c13476b0cd2aa7458facb31"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz#61b9ba09ea219e0174b3f35a6ad2afc94bdd5662"
|
||||||
integrity sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==
|
integrity sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64@4.55.1":
|
"@rollup/rollup-openbsd-x64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz#223e71224746a59ce6d955bbc403577bb5a8be9d"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz#fc4e54133134c1787d0b016ffdd5aeb22a5effd3"
|
||||||
integrity sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==
|
integrity sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64@4.55.1":
|
"@rollup/rollup-openharmony-arm64@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz#0817e5d8ecbfeb8b7939bf58f8ce3c9dd67fce77"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz#959ae225b1eeea0cc5b7c9f88e4834330fb6cd09"
|
||||||
integrity sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==
|
integrity sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.55.1":
|
"@rollup/rollup-win32-arm64-msvc@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz#de56d8f2013c84570ef5fb917aae034abda93e4a"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz#842acd38869fa1cbdbc240c76c67a86f93444c27"
|
||||||
integrity sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==
|
integrity sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.55.1":
|
"@rollup/rollup-win32-ia32-msvc@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz#659aff5244312475aeea2c9479a6c7d397b517bf"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz#7ab654def4042df44cb29f8ed9d5044e850c66d5"
|
||||||
integrity sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==
|
integrity sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu@4.55.1":
|
"@rollup/rollup-win32-x64-gnu@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz#2cb09549cbb66c1b979f9238db6dd454cac14a88"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz#7426cdec1b01d2382ffd5cda83cbdd1c8efb3ca6"
|
||||||
integrity sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==
|
integrity sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc@4.55.1":
|
"@rollup/rollup-win32-x64-msvc@4.56.0":
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz#f79437939020b83057faf07e98365b1fa51c458b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz#9eec0212732a432c71bde0350bc40b673d15b2db"
|
||||||
integrity sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==
|
integrity sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==
|
||||||
|
|
||||||
"@socket.io/component-emitter@~3.1.0":
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
@@ -1896,9 +1896,9 @@
|
|||||||
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "25.0.8"
|
version "25.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.8.tgz#e54e00f94fe1db2497b3e42d292b8376a2678c8d"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7"
|
||||||
integrity sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==
|
integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.16.0"
|
undici-types "~7.16.0"
|
||||||
|
|
||||||
@@ -1919,11 +1919,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
|
||||||
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
|
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
|
||||||
|
|
||||||
"@types/web-bluetooth@^0.0.17":
|
|
||||||
version "0.0.17"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz#5c9f3c617f64a9735d7b72a7cc671e166d900c40"
|
|
||||||
integrity sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==
|
|
||||||
|
|
||||||
"@types/web-bluetooth@^0.0.20":
|
"@types/web-bluetooth@^0.0.20":
|
||||||
version "0.0.20"
|
version "0.0.20"
|
||||||
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
|
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
|
||||||
@@ -1953,100 +1948,90 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz#164b36653910d27c130cf6c945b4bd9bde5bcbee"
|
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz#164b36653910d27c130cf6c945b4bd9bde5bcbee"
|
||||||
integrity sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==
|
integrity sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==
|
||||||
|
|
||||||
"@vue/compiler-core@3.5.26":
|
"@vue/compiler-core@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz#1a91ea90980528bedff7b1c292690bfb30612485"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz#ce4402428e26095586eb889c41f6e172eb3960bd"
|
||||||
integrity sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==
|
integrity sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/parser" "^7.28.5"
|
"@babel/parser" "^7.28.5"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
entities "^7.0.0"
|
entities "^7.0.0"
|
||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
"@vue/compiler-dom@3.5.26":
|
"@vue/compiler-dom@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz#66c36b6ed8bdf43236d7188ea332bc9d078eb286"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz#32b2bc87f0a652c253986796ace0ed6213093af8"
|
||||||
integrity sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==
|
integrity sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-core" "3.5.26"
|
"@vue/compiler-core" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
|
|
||||||
"@vue/compiler-sfc@3.5.26":
|
"@vue/compiler-sfc@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz#fb1c6c4bf9a9e22bb169e039e19437cb6995917a"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz#84651b8816bf8e7d6e62fddd14db86efd6d6f1b6"
|
||||||
integrity sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==
|
integrity sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/parser" "^7.28.5"
|
"@babel/parser" "^7.28.5"
|
||||||
"@vue/compiler-core" "3.5.26"
|
"@vue/compiler-core" "3.5.27"
|
||||||
"@vue/compiler-dom" "3.5.26"
|
"@vue/compiler-dom" "3.5.27"
|
||||||
"@vue/compiler-ssr" "3.5.26"
|
"@vue/compiler-ssr" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
magic-string "^0.30.21"
|
magic-string "^0.30.21"
|
||||||
postcss "^8.5.6"
|
postcss "^8.5.6"
|
||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
"@vue/compiler-ssr@3.5.26":
|
"@vue/compiler-ssr@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz#f6e94bccbb5339180779036ddfb614f998a197ea"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz#b480cad09dacf8f3d9c82b9843402f1a803baee7"
|
||||||
integrity sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==
|
integrity sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-dom" "3.5.26"
|
"@vue/compiler-dom" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
|
|
||||||
"@vue/devtools-api@^6.5.0":
|
"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4":
|
||||||
version "6.6.4"
|
version "6.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||||
|
|
||||||
"@vue/reactivity@3.5.26":
|
"@vue/reactivity@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.26.tgz#59a1edf566dc80133c1c26c93711c877e8602c48"
|
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.27.tgz#d870557de1389a27b8abcb7cbfa30978dc69a000"
|
||||||
integrity sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==
|
integrity sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
|
|
||||||
"@vue/runtime-core@3.5.26":
|
"@vue/runtime-core@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz#3f2c040bcf8018c03a1ab5adb0d788c13c986f0e"
|
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz#bb43744ed070166c7d581b849ac22b71a9ccf127"
|
||||||
integrity sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==
|
integrity sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/reactivity" "3.5.26"
|
"@vue/reactivity" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
|
|
||||||
"@vue/runtime-dom@3.5.26":
|
"@vue/runtime-dom@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz#5954848614883948ecc1f631a67b32cc32f81936"
|
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz#392513252c7ca7e5277240fdc70b8093449127f5"
|
||||||
integrity sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==
|
integrity sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/reactivity" "3.5.26"
|
"@vue/reactivity" "3.5.27"
|
||||||
"@vue/runtime-core" "3.5.26"
|
"@vue/runtime-core" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
csstype "^3.2.3"
|
csstype "^3.2.3"
|
||||||
|
|
||||||
"@vue/server-renderer@3.5.26":
|
"@vue/server-renderer@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz#269055497fcc75b3984063f866f17c748b565ef4"
|
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz#8137d0d7ec3b59d5992bb04c553775d209dddba7"
|
||||||
integrity sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==
|
integrity sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-ssr" "3.5.26"
|
"@vue/compiler-ssr" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
|
|
||||||
"@vue/shared@3.5.26":
|
"@vue/shared@3.5.27":
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
|
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.27.tgz#33a63143d8fb9ca1b3efbc7ecf9bd0ab05f7e06e"
|
||||||
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
|
integrity sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==
|
||||||
|
|
||||||
"@vueuse/core@10.4.1":
|
|
||||||
version "10.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.4.1.tgz#fc2c8a83a571c207aaedbe393b22daa6d35123f2"
|
|
||||||
integrity sha512-DkHIfMIoSIBjMgRRvdIvxsyboRZQmImofLyOHADqiVbQVilP8VVHDhBX2ZqoItOgu7dWa8oXiNnScOdPLhdEXg==
|
|
||||||
dependencies:
|
|
||||||
"@types/web-bluetooth" "^0.0.17"
|
|
||||||
"@vueuse/metadata" "10.4.1"
|
|
||||||
"@vueuse/shared" "10.4.1"
|
|
||||||
vue-demi ">=0.14.5"
|
|
||||||
|
|
||||||
"@vueuse/core@^10.11.0", "@vueuse/core@^10.4.1":
|
"@vueuse/core@^10.11.0", "@vueuse/core@^10.4.1":
|
||||||
version "10.11.1"
|
version "10.11.1"
|
||||||
@@ -2068,28 +2053,29 @@
|
|||||||
"@vueuse/shared" "12.8.2"
|
"@vueuse/shared" "12.8.2"
|
||||||
vue "^3.5.13"
|
vue "^3.5.13"
|
||||||
|
|
||||||
|
"@vueuse/core@^14.1.0":
|
||||||
|
version "14.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-14.1.0.tgz#274e98e591a505333b7dfb2bcaf7b4530a10b9c9"
|
||||||
|
integrity sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==
|
||||||
|
dependencies:
|
||||||
|
"@types/web-bluetooth" "^0.0.21"
|
||||||
|
"@vueuse/metadata" "14.1.0"
|
||||||
|
"@vueuse/shared" "14.1.0"
|
||||||
|
|
||||||
"@vueuse/metadata@10.11.1":
|
"@vueuse/metadata@10.11.1":
|
||||||
version "10.11.1"
|
version "10.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
|
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
|
||||||
integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
|
integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
|
||||||
|
|
||||||
"@vueuse/metadata@10.4.1":
|
|
||||||
version "10.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.4.1.tgz#9d2ff5c67abf17a8c07865c2413fbd0e92f7b7d7"
|
|
||||||
integrity sha512-2Sc8X+iVzeuMGHr6O2j4gv/zxvQGGOYETYXEc41h0iZXIRnRbJZGmY/QP8dvzqUelf8vg0p/yEA5VpCEu+WpZg==
|
|
||||||
|
|
||||||
"@vueuse/metadata@12.8.2":
|
"@vueuse/metadata@12.8.2":
|
||||||
version "12.8.2"
|
version "12.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3"
|
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3"
|
||||||
integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==
|
integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==
|
||||||
|
|
||||||
"@vueuse/router@12.7.0":
|
"@vueuse/metadata@14.1.0":
|
||||||
version "12.7.0"
|
version "14.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-12.7.0.tgz#b349b66e337057bb489b6d64d2dab044d41ca74d"
|
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-14.1.0.tgz#70fc2e94775e4a07369f11f86f6f0a465b04a381"
|
||||||
integrity sha512-Jp6dIel54oc2nh++zqjY06ipCcTT6YWDCNQ8dSSnqRwx90wIl7w7MQP7Wpp1wrDwXEoqhelfeZf2gjfrkAhq3g==
|
integrity sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==
|
||||||
dependencies:
|
|
||||||
"@vueuse/shared" "12.7.0"
|
|
||||||
vue "^3.5.13"
|
|
||||||
|
|
||||||
"@vueuse/shared@10.11.1", "@vueuse/shared@^10.11.0":
|
"@vueuse/shared@10.11.1", "@vueuse/shared@^10.11.0":
|
||||||
version "10.11.1"
|
version "10.11.1"
|
||||||
@@ -2098,20 +2084,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue-demi ">=0.14.8"
|
vue-demi ">=0.14.8"
|
||||||
|
|
||||||
"@vueuse/shared@10.4.1":
|
|
||||||
version "10.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.4.1.tgz#d5ce33033c156efb60664b5d6034d6cd4e2f530c"
|
|
||||||
integrity sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg==
|
|
||||||
dependencies:
|
|
||||||
vue-demi ">=0.14.5"
|
|
||||||
|
|
||||||
"@vueuse/shared@12.7.0":
|
|
||||||
version "12.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.7.0.tgz#0c573789069818a2e25ddae3ab64b536c614537b"
|
|
||||||
integrity sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==
|
|
||||||
dependencies:
|
|
||||||
vue "^3.5.13"
|
|
||||||
|
|
||||||
"@vueuse/shared@12.8.2", "@vueuse/shared@^12.5.0":
|
"@vueuse/shared@12.8.2", "@vueuse/shared@^12.5.0":
|
||||||
version "12.8.2"
|
version "12.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930"
|
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930"
|
||||||
@@ -2119,6 +2091,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue "^3.5.13"
|
vue "^3.5.13"
|
||||||
|
|
||||||
|
"@vueuse/shared@14.1.0":
|
||||||
|
version "14.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.1.0.tgz#49b2face86a9c0c52e20eaf4c732a0223276c11f"
|
||||||
|
integrity sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==
|
||||||
|
|
||||||
"@yr/monotone-cubic-spline@^1.0.3":
|
"@yr/monotone-cubic-spline@^1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
|
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
|
||||||
@@ -2288,9 +2265,9 @@ base64-js@^1.3.1:
|
|||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
|
|
||||||
baseline-browser-mapping@^2.9.0:
|
baseline-browser-mapping@^2.9.0:
|
||||||
version "2.9.14"
|
version "2.9.17"
|
||||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz#3b6af0bc032445bca04de58caa9a87cfe921cbb3"
|
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz#9d6019766cd7eba738cb5f32c84b9f937cc87780"
|
||||||
integrity sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==
|
integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==
|
||||||
|
|
||||||
binary-extensions@^2.0.0:
|
binary-extensions@^2.0.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
@@ -2328,7 +2305,7 @@ braces@^3.0.3, braces@~3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
browserslist@^4.19.1, browserslist@^4.24.0, browserslist@^4.28.0:
|
browserslist@^4.19.1, browserslist@^4.24.0, browserslist@^4.28.1:
|
||||||
version "4.28.1"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
|
||||||
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
|
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
|
||||||
@@ -2389,9 +2366,9 @@ camelcase-css@^2.0.1:
|
|||||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001759:
|
caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001759:
|
||||||
version "1.0.30001764"
|
version "1.0.30001765"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz#03206c56469f236103b90f9ae10bcb8b9e1f6005"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz#4a78d8a797fd4124ebaab2043df942eb091648ee"
|
||||||
integrity sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==
|
integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==
|
||||||
|
|
||||||
chalk@^4.1.0:
|
chalk@^4.1.0:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
@@ -2506,16 +2483,16 @@ convert-source-map@^2.0.0:
|
|||||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||||
|
|
||||||
core-js-compat@^3.43.0:
|
core-js-compat@^3.43.0:
|
||||||
version "3.47.0"
|
version "3.48.0"
|
||||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3"
|
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6"
|
||||||
integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==
|
integrity sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist "^4.28.0"
|
browserslist "^4.28.1"
|
||||||
|
|
||||||
core-js@^3.1.3, core-js@^3.26.1:
|
core-js@^3.1.3, core-js@^3.26.1:
|
||||||
version "3.47.0"
|
version "3.48.0"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.47.0.tgz#436ef07650e191afeb84c24481b298bd60eb4a17"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.48.0.tgz#1f813220a47bbf0e667e3885c36cd6f0593bf14d"
|
||||||
integrity sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==
|
integrity sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==
|
||||||
|
|
||||||
crelt@^1.0.0, crelt@^1.0.5, crelt@^1.0.6:
|
crelt@^1.0.0, crelt@^1.0.5, crelt@^1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
@@ -2689,9 +2666,9 @@ ejs@^3.1.6:
|
|||||||
jake "^10.8.5"
|
jake "^10.8.5"
|
||||||
|
|
||||||
electron-to-chromium@^1.5.263:
|
electron-to-chromium@^1.5.263:
|
||||||
version "1.5.267"
|
version "1.5.277"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz#7164191a07bf32a7e646e68334f402dd60629821"
|
||||||
integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==
|
integrity sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==
|
||||||
|
|
||||||
engine.io-client@~6.5.2:
|
engine.io-client@~6.5.2:
|
||||||
version "6.5.4"
|
version "6.5.4"
|
||||||
@@ -2726,9 +2703,9 @@ entities@^4.4.0:
|
|||||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||||
|
|
||||||
entities@^7.0.0:
|
entities@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a"
|
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
|
||||||
integrity sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==
|
integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
|
||||||
|
|
||||||
es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
|
es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
|
||||||
version "1.24.1"
|
version "1.24.1"
|
||||||
@@ -2977,10 +2954,10 @@ fraction.js@^4.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
frappe-ui@^0.1.256:
|
frappe-ui@^0.1.261:
|
||||||
version "0.1.256"
|
version "0.1.261"
|
||||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.256.tgz#c14756eda75ca01ada034559e8bd2f91bcfe6dff"
|
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.261.tgz#d6919c713a37ed8a2bdb667707dba9ece4956c6d"
|
||||||
integrity sha512-zj8n6KXpMv/0h1NcaCsjFLP8QBnofDEBJgQa+xECU0/jbq4gSqNhFOkcx788qNL+vmBo9frywTeXwDpl7hUCZA==
|
integrity sha512-sEdEAgjAkrTERYWk5HBOQuKa7/xuex/X8/Y/hCYFbEThwwy2ZWmQOCsTNyOCjXAn7lyV49Ues/TW01koIq/ysQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/vue" "^1.1.6"
|
"@floating-ui/vue" "^1.1.6"
|
||||||
"@headlessui/vue" "^1.7.14"
|
"@headlessui/vue" "^1.7.14"
|
||||||
@@ -3613,9 +3590,9 @@ lodash.sortby@^4.7.0:
|
|||||||
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
|
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
|
||||||
|
|
||||||
lodash@^4.17.20:
|
lodash@^4.17.20:
|
||||||
version "4.17.21"
|
version "4.17.23"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
|
||||||
|
|
||||||
log-symbols@^4.1.0:
|
log-symbols@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
@@ -4032,9 +4009,9 @@ postcss@^8.4.32, postcss@^8.4.47, postcss@^8.5.6:
|
|||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
prettier@^3.3.2:
|
prettier@^3.3.2:
|
||||||
version "3.7.4"
|
version "3.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
|
||||||
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
|
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==
|
||||||
|
|
||||||
pretty-bytes@^5.3.0:
|
pretty-bytes@^5.3.0:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
@@ -4115,9 +4092,9 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2, prosemirror-keymap@^1.2.3:
|
|||||||
w3c-keyname "^2.2.0"
|
w3c-keyname "^2.2.0"
|
||||||
|
|
||||||
prosemirror-markdown@^1.13.1:
|
prosemirror-markdown@^1.13.1:
|
||||||
version "1.13.2"
|
version "1.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503"
|
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.3.tgz#cf38e98f10c432b906bfcc7179c2e3ab58f49362"
|
||||||
integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==
|
integrity sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/markdown-it" "^14.0.0"
|
"@types/markdown-it" "^14.0.0"
|
||||||
markdown-it "^14.0.0"
|
markdown-it "^14.0.0"
|
||||||
@@ -4185,16 +4162,16 @@ prosemirror-trailing-node@^3.0.0:
|
|||||||
escape-string-regexp "^4.0.0"
|
escape-string-regexp "^4.0.0"
|
||||||
|
|
||||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
|
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
|
||||||
version "1.10.5"
|
version "1.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz#4cf9fe5dcbdbfebd62499f24386e7cec9bc9979b"
|
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz#f5c5050354423dc83c6b083f6f1959ec86a3f9ba"
|
||||||
integrity sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==
|
integrity sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model "^1.21.0"
|
prosemirror-model "^1.21.0"
|
||||||
|
|
||||||
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.41.4:
|
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.41.4:
|
||||||
version "1.41.4"
|
version "1.41.5"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.4.tgz#4e1b3e90accc0eebe3bddb497a40ce54e4de722d"
|
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.5.tgz#3e152d14af633f2f5a73aba24e6130c63f643b2b"
|
||||||
integrity sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==
|
integrity sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model "^1.20.0"
|
prosemirror-model "^1.20.0"
|
||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
@@ -4395,37 +4372,37 @@ rollup@^2.43.1:
|
|||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
rollup@^4.2.0:
|
rollup@^4.2.0:
|
||||||
version "4.55.1"
|
version "4.56.0"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.55.1.tgz#4ec182828be440648e7ee6520dc35e9f20e05144"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.56.0.tgz#65959d13cfbd7e48b8868c05165b1738f0143862"
|
||||||
integrity sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==
|
integrity sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "1.0.8"
|
"@types/estree" "1.0.8"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@rollup/rollup-android-arm-eabi" "4.55.1"
|
"@rollup/rollup-android-arm-eabi" "4.56.0"
|
||||||
"@rollup/rollup-android-arm64" "4.55.1"
|
"@rollup/rollup-android-arm64" "4.56.0"
|
||||||
"@rollup/rollup-darwin-arm64" "4.55.1"
|
"@rollup/rollup-darwin-arm64" "4.56.0"
|
||||||
"@rollup/rollup-darwin-x64" "4.55.1"
|
"@rollup/rollup-darwin-x64" "4.56.0"
|
||||||
"@rollup/rollup-freebsd-arm64" "4.55.1"
|
"@rollup/rollup-freebsd-arm64" "4.56.0"
|
||||||
"@rollup/rollup-freebsd-x64" "4.55.1"
|
"@rollup/rollup-freebsd-x64" "4.56.0"
|
||||||
"@rollup/rollup-linux-arm-gnueabihf" "4.55.1"
|
"@rollup/rollup-linux-arm-gnueabihf" "4.56.0"
|
||||||
"@rollup/rollup-linux-arm-musleabihf" "4.55.1"
|
"@rollup/rollup-linux-arm-musleabihf" "4.56.0"
|
||||||
"@rollup/rollup-linux-arm64-gnu" "4.55.1"
|
"@rollup/rollup-linux-arm64-gnu" "4.56.0"
|
||||||
"@rollup/rollup-linux-arm64-musl" "4.55.1"
|
"@rollup/rollup-linux-arm64-musl" "4.56.0"
|
||||||
"@rollup/rollup-linux-loong64-gnu" "4.55.1"
|
"@rollup/rollup-linux-loong64-gnu" "4.56.0"
|
||||||
"@rollup/rollup-linux-loong64-musl" "4.55.1"
|
"@rollup/rollup-linux-loong64-musl" "4.56.0"
|
||||||
"@rollup/rollup-linux-ppc64-gnu" "4.55.1"
|
"@rollup/rollup-linux-ppc64-gnu" "4.56.0"
|
||||||
"@rollup/rollup-linux-ppc64-musl" "4.55.1"
|
"@rollup/rollup-linux-ppc64-musl" "4.56.0"
|
||||||
"@rollup/rollup-linux-riscv64-gnu" "4.55.1"
|
"@rollup/rollup-linux-riscv64-gnu" "4.56.0"
|
||||||
"@rollup/rollup-linux-riscv64-musl" "4.55.1"
|
"@rollup/rollup-linux-riscv64-musl" "4.56.0"
|
||||||
"@rollup/rollup-linux-s390x-gnu" "4.55.1"
|
"@rollup/rollup-linux-s390x-gnu" "4.56.0"
|
||||||
"@rollup/rollup-linux-x64-gnu" "4.55.1"
|
"@rollup/rollup-linux-x64-gnu" "4.56.0"
|
||||||
"@rollup/rollup-linux-x64-musl" "4.55.1"
|
"@rollup/rollup-linux-x64-musl" "4.56.0"
|
||||||
"@rollup/rollup-openbsd-x64" "4.55.1"
|
"@rollup/rollup-openbsd-x64" "4.56.0"
|
||||||
"@rollup/rollup-openharmony-arm64" "4.55.1"
|
"@rollup/rollup-openharmony-arm64" "4.56.0"
|
||||||
"@rollup/rollup-win32-arm64-msvc" "4.55.1"
|
"@rollup/rollup-win32-arm64-msvc" "4.56.0"
|
||||||
"@rollup/rollup-win32-ia32-msvc" "4.55.1"
|
"@rollup/rollup-win32-ia32-msvc" "4.56.0"
|
||||||
"@rollup/rollup-win32-x64-gnu" "4.55.1"
|
"@rollup/rollup-win32-x64-gnu" "4.56.0"
|
||||||
"@rollup/rollup-win32-x64-msvc" "4.55.1"
|
"@rollup/rollup-win32-x64-msvc" "4.56.0"
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
rope-sequence@^1.3.0:
|
rope-sequence@^1.3.0:
|
||||||
@@ -4797,9 +4774,9 @@ tempy@^0.6.0:
|
|||||||
unique-string "^2.0.0"
|
unique-string "^2.0.0"
|
||||||
|
|
||||||
terser@^5.0.0:
|
terser@^5.0.0:
|
||||||
version "5.44.1"
|
version "5.46.0"
|
||||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c"
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695"
|
||||||
integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==
|
integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/source-map" "^0.3.3"
|
"@jridgewell/source-map" "^0.3.3"
|
||||||
acorn "^8.15.0"
|
acorn "^8.15.0"
|
||||||
@@ -4940,9 +4917,9 @@ uc.micro@^2.0.0, uc.micro@^2.1.0:
|
|||||||
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
|
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
|
||||||
|
|
||||||
ufo@^1.6.1:
|
ufo@^1.6.1:
|
||||||
version "1.6.2"
|
version "1.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.2.tgz#aaf4d46b98425b2fb5031abe8d65ca069e93e755"
|
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.3.tgz#799666e4e88c122a9659805e30b9dc071c3aed4f"
|
||||||
integrity sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==
|
integrity sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==
|
||||||
|
|
||||||
unbox-primitive@^1.1.0:
|
unbox-primitive@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@@ -5169,7 +5146,7 @@ vue-codemirror@6.1.1:
|
|||||||
"@codemirror/state" "6.x"
|
"@codemirror/state" "6.x"
|
||||||
"@codemirror/view" "6.x"
|
"@codemirror/view" "6.x"
|
||||||
|
|
||||||
vue-demi@*, vue-demi@>=0.13.0, vue-demi@>=0.14.5, vue-demi@>=0.14.8:
|
vue-demi@*, vue-demi@>=0.13.0, vue-demi@>=0.14.8:
|
||||||
version "0.14.10"
|
version "0.14.10"
|
||||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
||||||
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
|
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
|
||||||
@@ -5179,28 +5156,28 @@ vue-draggable-next@2.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/vue-draggable-next/-/vue-draggable-next-2.2.1.tgz#adbe98c74610cca8f4eb63f92042681f96920451"
|
resolved "https://registry.yarnpkg.com/vue-draggable-next/-/vue-draggable-next-2.2.1.tgz#adbe98c74610cca8f4eb63f92042681f96920451"
|
||||||
integrity sha512-EAMS1IRHF0kZO0o5PMOinsQsXIqsrKT1hKmbICxG3UEtn7zLFkLxlAtajcCcUTisNvQ6TtCB5COjD9a1raNADw==
|
integrity sha512-EAMS1IRHF0kZO0o5PMOinsQsXIqsrKT1hKmbICxG3UEtn7zLFkLxlAtajcCcUTisNvQ6TtCB5COjD9a1raNADw==
|
||||||
|
|
||||||
vue-router@4.2.2:
|
vue-router@^4.6.4:
|
||||||
version "4.2.2"
|
version "4.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.2.tgz#b0097b66d89ca81c0986be03da244c7b32a4fd81"
|
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
|
||||||
integrity sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==
|
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/devtools-api" "^6.5.0"
|
"@vue/devtools-api" "^6.6.4"
|
||||||
|
|
||||||
vue3-apexcharts@1.8.0:
|
vue3-apexcharts@1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.8.0.tgz#1984648d966aa91bc4dc3e87fa847f5289f7f1cf"
|
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.8.0.tgz#1984648d966aa91bc4dc3e87fa847f5289f7f1cf"
|
||||||
integrity sha512-5tSD4mXTBbIJ9ir+58qHE6oNtIe0RNgqIRYMKpcsIaxkKtwUww4JhvPkpUFlmiW4OJbbdklgjleXq1lfcM4gdA==
|
integrity sha512-5tSD4mXTBbIJ9ir+58qHE6oNtIe0RNgqIRYMKpcsIaxkKtwUww4JhvPkpUFlmiW4OJbbdklgjleXq1lfcM4gdA==
|
||||||
|
|
||||||
vue@^3.5.0, vue@^3.5.13:
|
vue@^3.5.13, vue@^3.5.27:
|
||||||
version "3.5.26"
|
version "3.5.27"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.26.tgz#03a0b17311e0e593d34b9358fa249b85e3a6d9fb"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.27.tgz#e55fd941b614459ab2228489bc19d1692e05876c"
|
||||||
integrity sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==
|
integrity sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-dom" "3.5.26"
|
"@vue/compiler-dom" "3.5.27"
|
||||||
"@vue/compiler-sfc" "3.5.26"
|
"@vue/compiler-sfc" "3.5.27"
|
||||||
"@vue/runtime-dom" "3.5.26"
|
"@vue/runtime-dom" "3.5.27"
|
||||||
"@vue/server-renderer" "3.5.26"
|
"@vue/server-renderer" "3.5.27"
|
||||||
"@vue/shared" "3.5.26"
|
"@vue/shared" "3.5.27"
|
||||||
|
|
||||||
vuedraggable@4.1.0:
|
vuedraggable@4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
@@ -5281,9 +5258,9 @@ which-collection@^1.0.2:
|
|||||||
is-weakset "^2.0.3"
|
is-weakset "^2.0.3"
|
||||||
|
|
||||||
which-typed-array@^1.1.16, which-typed-array@^1.1.19:
|
which-typed-array@^1.1.16, which-typed-array@^1.1.19:
|
||||||
version "1.1.19"
|
version "1.1.20"
|
||||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956"
|
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122"
|
||||||
integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==
|
integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==
|
||||||
dependencies:
|
dependencies:
|
||||||
available-typed-arrays "^1.0.7"
|
available-typed-arrays "^1.0.7"
|
||||||
call-bind "^1.0.8"
|
call-bind "^1.0.8"
|
||||||
|
|||||||
103
lms/auth.py
Normal file
103
lms/auth.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
ALLOWED_PATHS = [
|
||||||
|
"/api/method/ping",
|
||||||
|
"/api/method/login",
|
||||||
|
"/api/method/logout",
|
||||||
|
"/api/method/frappe.core.doctype.communication.email.mark_email_as_seen",
|
||||||
|
"/api/method/frappe.realtime.get_user_info",
|
||||||
|
"/api/method/frappe.realtime.can_subscribe_doc",
|
||||||
|
"/api/method/frappe.realtime.can_subscribe_doctype",
|
||||||
|
"/api/method/frappe.realtime.has_permission",
|
||||||
|
"/api/method/frappe.integrations.oauth2.authorize",
|
||||||
|
"/api/method/frappe.integrations.oauth2.approve",
|
||||||
|
"/api/method/frappe.integrations.oauth2.get_token",
|
||||||
|
"/api/method/frappe.www.login.login_via_google",
|
||||||
|
"/api/method/frappe.www.login.login_via_github",
|
||||||
|
"/api/method/frappe.www.login.login_via_facebook",
|
||||||
|
"/api/method/frappe.www.login.login_via_frappe",
|
||||||
|
"/api/method/frappe.www.login.login_via_office365",
|
||||||
|
"/api/method/frappe.www.login.login_via_salesforce",
|
||||||
|
"/api/method/frappe.www.login.login_via_fairlogin",
|
||||||
|
"/api/method/frappe.www.login.login_via_keycloak",
|
||||||
|
"/api/method/frappe.www.login.custom",
|
||||||
|
"/api/method/frappe.integrations.oauth2.openid_profile",
|
||||||
|
"/api/method/frappe.website.doctype.web_page_view.web_page_view.make_view_log",
|
||||||
|
"/api/method/upload_file",
|
||||||
|
"/api/method/frappe.search.web_search",
|
||||||
|
"/api/method/frappe.email.queue.unsubscribe",
|
||||||
|
"/api/method/frappe.website.doctype.web_form.web_form.accept",
|
||||||
|
"/api/method/frappe.core.doctype.user.user.test_password_strength",
|
||||||
|
"/api/method/frappe.core.doctype.user.user.update_password",
|
||||||
|
"/api/method/frappe.utils.telemetry.pulse.client.is_enabled",
|
||||||
|
"/api/method/frappe.client.get_value",
|
||||||
|
"/api/method/frappe.client.get_count",
|
||||||
|
"/api/method/frappe.client.get",
|
||||||
|
"/api/method/frappe.client.insert",
|
||||||
|
"/api/method/frappe.client.set_value",
|
||||||
|
"/api/method/frappe.client.delete",
|
||||||
|
"/api/method/frappe.client.get_list",
|
||||||
|
"/api/method/frappe.client.rename_doc",
|
||||||
|
"/api/method/frappe.onboarding.get_onboarding_status",
|
||||||
|
"/api/method/frappe.utils.print_format.download_pdf",
|
||||||
|
"/api/method/frappe.desk.search.search_link",
|
||||||
|
"/api/method/frappe.core.doctype.communication.email.make",
|
||||||
|
"/api/method/frappe.core.doctype.user.user.reset_password",
|
||||||
|
"/api/method/frappe.desk.doctype.notification_log.notification_log.mark_as_read",
|
||||||
|
"/api/method/frappe.desk.doctype.notification_log.notification_log.mark_all_as_read",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate():
|
||||||
|
if not frappe.conf.get("block_endpoints"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.form_dict.cmd:
|
||||||
|
path = f"/api/method/{frappe.form_dict.cmd}"
|
||||||
|
else:
|
||||||
|
path = frappe.request.path
|
||||||
|
|
||||||
|
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
|
||||||
|
if user_type == "System User":
|
||||||
|
return
|
||||||
|
|
||||||
|
if not path.startswith("/api/"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if path.startswith("/lms") or path.startswith("/api/method/lms."):
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_server_script_path(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_custom_app_endpoint(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
if path in ALLOWED_PATHS:
|
||||||
|
return
|
||||||
|
frappe.throw(f"Access not allowed for this URL: {path}", frappe.PermissionError)
|
||||||
|
|
||||||
|
|
||||||
|
def is_server_script_path(path):
|
||||||
|
endpoint = path.split("/api/method/")[-1]
|
||||||
|
if frappe.db.exists("Server Script", {"script_type": "API", "api_method": endpoint, "disabled": 0}):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_custom_app_endpoint(path):
|
||||||
|
allowed_custom_endpoints = frappe.conf.get("allowed_custom_endpoints", [])
|
||||||
|
|
||||||
|
if isinstance(allowed_custom_endpoints, str):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(allowed_custom_endpoints)
|
||||||
|
allowed_custom_endpoints = parsed if isinstance(parsed, list) else [allowed_custom_endpoints]
|
||||||
|
except Exception:
|
||||||
|
allowed_custom_endpoints = [allowed_custom_endpoints]
|
||||||
|
|
||||||
|
for endpoint in allowed_custom_endpoints:
|
||||||
|
if endpoint in path:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user