feat: configurable frontend base path

Co-authored-by: Suraj Shetty <surajshetty3416@users.noreply.github.com>
This commit is contained in:
Hussain Nagaria
2026-01-17 23:04:31 +05:30
parent 376de99ef7
commit fe1aa3dd40
36 changed files with 177 additions and 78 deletions

View File

@@ -7,7 +7,7 @@
"dev": "vite",
"serve": "vite preview",
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry && yarn copy-colors-json",
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html",
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/_lms.html",
"copy-colors-json": "cp node_modules/frappe-ui/tailwind/colors.json src/utils/frappe-ui-colors.json"
},
"dependencies": {

View File

@@ -65,6 +65,7 @@ import { Dialog, FormControl } from 'frappe-ui'
import { nextTick, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { Link } from 'frappe-ui/frappe'
import { getLmsRoute } from '@/utils/basePath'
const show = ref(false)
const quiz = ref(null)
@@ -94,7 +95,10 @@ const addAssessment = () => {
}
const redirectToForm = () => {
if (props.type == 'quiz') window.open('/lms/quizzes?new=true', '_blank')
else window.open('/lms/assignments?new=true', '_blank')
if (props.type == 'quiz') {
window.open(getLmsRoute('quizzes?new=true'), '_blank')
} else {
window.open(getLmsRoute('assignments?new=true'), '_blank')
}
}
</script>

View File

@@ -248,6 +248,7 @@ import DateRange from '@/components/Common/DateRange.vue'
import BulkCertificates from '@/components/Modals/BulkCertificates.vue'
import BatchFeedback from '@/components/BatchFeedback.vue'
import dayjs from 'dayjs/esm'
import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const showAnnouncementModal = ref(false)
@@ -357,7 +358,9 @@ const isStudent = computed(() => {
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
window.location.href = `/login?redirect-to=${getLmsRoute(
`batches/${props.batchName}`
)}`
}
const openAnnouncementModal = () => {

View File

@@ -207,14 +207,18 @@
:text="access.data.message"
:buttonLabel="type == 'course' ? 'Checkout Course' : 'Checkout Batch'"
:buttonLink="
type == 'course' ? `/lms/courses/${name}` : `/lms/batches/${name}`
type == 'course'
? getLmsRoute(`courses/${name}`)
: getLmsRoute(`batches/${name}`)
"
/>
</div>
<div v-else-if="!user.data?.name">
<NotPermitted
text="Please login to access this page."
:buttonLink="`/login?redirect-to=/lms/billing/${type}/${name}`"
:buttonLink="`/login?redirect-to=${getLmsRoute(
`billing/${type}/${name}`
)}`"
/>
</div>
</div>
@@ -235,6 +239,7 @@ import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
import { X } from 'lucide-vue-next'
import { useTelemetry } from 'frappe-ui/frappe'
import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const { brand } = sessionStore()
@@ -441,11 +446,11 @@ const changeCurrency = (country) => {
const redirectTo = computed(() => {
if (props.type == 'course') {
return `/lms/courses/${props.name}`
return getLmsRoute(`courses/${props.name}`)
} else if (props.type == 'batch') {
return `/lms/batches/${props.name}`
return getLmsRoute(`batches/${props.name}`)
} else if (props.type == 'certificate') {
return `/lms/courses/${props.name}/certification`
return getLmsRoute(`courses/${props.name}/certification`)
}
})

View File

@@ -378,6 +378,7 @@ import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import Notes from '@/components/Notes/Notes.vue'
import InlineLessonMenu from '@/components/Notes/InlineLessonMenu.vue'
import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const socket = inject('$socket')
@@ -902,7 +903,9 @@ watch(allowDiscussions, () => {
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
window.location.href = `/login?redirect-to=${getLmsRoute(
`courses/${props.courseName}`
)}`
}
usePageMeta(() => {

View File

@@ -122,6 +122,7 @@ import { X, LinkedinIcon, Twitter } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { decodeEntities } from '@/utils'
import DOMPurify from 'dompurify'
import { getLmsRoute } from '@/utils/basePath'
const dayjs = inject('$dayjs')
const { branding } = sessionStore()
@@ -158,7 +159,9 @@ const badges = createResource({
const shareOnSocial = (badge, medium) => {
let shareUrl
const url = encodeURIComponent(
`${window.location.origin}/lms/badges/${badge.badge}/${props.profile.data?.email}`
`${window.location.origin}${getLmsRoute(
`badges/${badge.badge}/${props.profile.data?.email}`
)}`
)
const summary = `I am happy to announce that I earned the ${
badge.badge

View File

@@ -158,6 +158,7 @@ import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import { openSettings } from '@/utils'
import { useSettings } from '@/stores/settings'
import { getLmsRoute } from '@/utils/basePath'
const user = inject<any>('$user')
const code = ref<string | null>('')
@@ -255,7 +256,10 @@ const updateBoilerPlate = () => {
const checkIfUserIsPermitted = (doc: any = null) => {
if (!user.data) {
window.location.href = `/login?redirect-to=/lms/programming-exercises/${props.exerciseID}/submission/${props.submissionID}`
const redirectPath = getLmsRoute(
`programming-exercises/${props.exerciseID}/submission/${props.submissionID}`
)
window.location.href = `/login?redirect-to=${redirectPath}`
}
if (!doc) return

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import { usersStore } from './stores/user'
import { sessionStore } from './stores/session'
import { useSettings } from './stores/settings'
import { getLmsBasePath } from './utils/basePath'
const routes = [
{
@@ -268,7 +269,7 @@ const routes = [
]
let router = createRouter({
history: createWebHistory('/lms'),
history: createWebHistory(`/${getLmsBasePath()}`),
routes,
})

View File

@@ -5,6 +5,7 @@ import translationPlugin from '../translation'
import { usersStore } from '@/stores/user'
import { call } from 'frappe-ui'
import router from '@/router'
import { getLmsRoute } from '@/utils/basePath'
export class Assignment {
constructor({ data, api, readOnly }) {
@@ -53,7 +54,10 @@ export class Assignment {
fieldname: ['name'],
}).then((data) => {
let submission = data.name || 'new'
this.wrapper.innerHTML = `<iframe src="/lms/assignment-submission/${assignment}/${submission}?fromLesson=1" class="w-full h-[500px]"></iframe>`
const submissionPath = getLmsRoute(
`assignment-submission/${assignment}/${submission}?fromLesson=1`
)
this.wrapper.innerHTML = `<iframe src="${submissionPath}" class="w-full h-[500px]"></iframe>`
})
return
}

View File

@@ -0,0 +1,12 @@
export function getLmsBasePath() {
return window.lms_path || 'lms'
}
export function getLmsRoute(path = '') {
const base = getLmsBasePath()
if (!path) {
return base
}
const normalized = path.startsWith('/') ? path.slice(1) : path
return `/${base}/${normalized}`
}

View File

@@ -4,6 +4,7 @@ import translationPlugin from '@/translation'
import ProgrammingExerciseModal from '@/pages/ProgrammingExercises/ProgrammingExerciseModal.vue';
import { call } from 'frappe-ui';
import { usersStore } from '@/stores/user'
import { getLmsRoute } from '@/utils/basePath'
export class Program {
@@ -73,7 +74,10 @@ export class Program {
fieldname: ['name'],
}).then((data: { name: string }) => {
let submission = data.name || 'new'
this.wrapper.innerHTML = `<iframe src="/lms/programming-exercises/${exercise}/submission/${submission}?fromLesson=1" class="w-full h-[900px] border rounded-md"></iframe>`
const submissionPath = getLmsRoute(
`programming-exercises/${exercise}/submission/${submission}?fromLesson=1`
)
this.wrapper.innerHTML = `<iframe src="${submissionPath}" class="w-full h-[900px] border rounded-md"></iframe>`
})
return
}
@@ -100,4 +104,4 @@ export class Program {
exercise: this.data.exercise,
}
}
}
}

View File

@@ -5,6 +5,7 @@ import { usersStore } from '../stores/user'
import translationPlugin from '../translation'
import { CircleHelp } from 'lucide-vue-next'
import router from '@/router'
import { getLmsRoute } from '@/utils/basePath'
export class Quiz {
constructor({ data, api, readOnly }) {
@@ -42,7 +43,8 @@ export class Quiz {
renderQuiz(quiz) {
if (this.readOnly) {
this.wrapper.innerHTML = `<iframe src="/lms/quiz/${quiz}?fromLesson=1" class="w-full h-[500px]"></iframe>`
const quizPath = getLmsRoute(`quiz/${quiz}?fromLesson=1`)
this.wrapper.innerHTML = `<iframe src="${quizPath}" class="w-full h-[500px]"></iframe>`
return
}
this.wrapper.innerHTML = `<div class='border rounded-md p-4 text-center bg-surface-menu-bar mb-4'>

View File

@@ -17,7 +17,7 @@ export default defineConfig(async ({ mode }) => {
lucideIcons: true,
jinjaBootData: true,
buildConfig: {
indexHtmlPath: '../lms/www/lms.html',
indexHtmlPath: '../lms/www/_lms.html',
},
}),
vue(),