Merge branch 'develop' of https://github.com/frappe/lms into full-text-search
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<Layout class="isolate text-base">
|
||||
<router-view />
|
||||
</Layout>
|
||||
<InstallPrompt v-if="isMobile" />
|
||||
<InstallPrompt v-if="isMobile && !settings.data?.disable_pwa" />
|
||||
<Dialogs />
|
||||
</FrappeUIProvider>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@ import { Dialogs } from '@/utils/dialogs'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useScreenSize } from './utils/composables'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { posthogSettings } from '@/telemetry'
|
||||
import DesktopLayout from './components/DesktopLayout.vue'
|
||||
@@ -24,6 +25,7 @@ const { isMobile } = useScreenSize()
|
||||
const router = useRouter()
|
||||
const noSidebar = ref(false)
|
||||
const { userResource } = usersStore()
|
||||
const { settings } = useSettings()
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.query.fromLesson || to.path === '/persona') {
|
||||
|
||||
@@ -48,7 +48,7 @@ const settingsStore = useSettings()
|
||||
|
||||
const sendMail = (close: Function) => {
|
||||
call('frappe.core.doctype.communication.email.make', {
|
||||
recipients: settingsStore.contactUsEmail?.data,
|
||||
recipients: settingsStore.settings?.data?.contact_us_email,
|
||||
subject: subject.value,
|
||||
content: message.value,
|
||||
send_email: true,
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { escapeHTML } from '@/utils'
|
||||
import { escapeHTML, sanitizeHTML } from '@/utils'
|
||||
|
||||
const show = defineModel()
|
||||
const assignments = defineModel<Assignments>('assignments')
|
||||
@@ -122,12 +122,13 @@ watch(show, (newVal) => {
|
||||
}
|
||||
})
|
||||
|
||||
const validateTitle = () => {
|
||||
const validateFields = () => {
|
||||
assignment.title = escapeHTML(assignment.title.trim())
|
||||
assignment.question = sanitizeHTML(assignment.question)
|
||||
}
|
||||
|
||||
const saveAssignment = () => {
|
||||
validateTitle()
|
||||
validateFields()
|
||||
if (props.assignmentID == 'new') {
|
||||
createAssignment()
|
||||
} else {
|
||||
|
||||
@@ -104,9 +104,8 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { getFileSize, decodeEntities } from '@/utils'
|
||||
import { getFileSize, sanitizeHTML } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
const reloadProfile = defineModel('reloadProfile')
|
||||
const hasLanguageChanged = ref(false)
|
||||
@@ -157,22 +156,7 @@ const updateProfile = createResource({
|
||||
})
|
||||
|
||||
const saveProfile = (close) => {
|
||||
profile.bio = DOMPurify.sanitize(decodeEntities(profile.bio), {
|
||||
ALLOWED_TAGS: [
|
||||
'b',
|
||||
'i',
|
||||
'em',
|
||||
'strong',
|
||||
'a',
|
||||
'p',
|
||||
'br',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'img',
|
||||
],
|
||||
ALLOWED_ATTR: ['href', 'target', 'src'],
|
||||
})
|
||||
profile.bio = sanitizeHTML(profile.bio)
|
||||
updateProfile.submit(
|
||||
{},
|
||||
{
|
||||
|
||||
@@ -120,6 +120,13 @@ const tabsStructure = computed(() => {
|
||||
description:
|
||||
'If enabled, users will no able to move forward in a video',
|
||||
},
|
||||
{
|
||||
label: 'Disable PWA',
|
||||
name: 'disable_pwa',
|
||||
type: 'checkbox',
|
||||
description:
|
||||
'If checked, users will not be able to install the application as a Progressive Web App.',
|
||||
},
|
||||
{
|
||||
label: 'Send calendar invite for evaluations',
|
||||
name: 'send_calendar_invite_for_evaluations',
|
||||
|
||||
@@ -240,8 +240,7 @@ const showPageModal = ref(false)
|
||||
const isModerator = ref(false)
|
||||
const isInstructor = ref(false)
|
||||
const pageToEdit = ref(null)
|
||||
const settingsStore = useSettings()
|
||||
const { sidebarSettings } = settingsStore
|
||||
const { settings, sidebarSettings, activeTab, isSettingsOpen } = useSettings()
|
||||
const showOnboarding = ref(false)
|
||||
const showIntermediateModal = ref(false)
|
||||
const currentStep = ref({})
|
||||
@@ -412,7 +411,7 @@ const addPrograms = async () => {
|
||||
}
|
||||
|
||||
const addContactUsDetails = () => {
|
||||
if (!settingsStore.contactUsEmail?.data && !settingsStore.contactUsURL?.data)
|
||||
if (!settings?.data?.contact_us_email && !settings?.data?.contact_us_url)
|
||||
return
|
||||
|
||||
const contactUsLinkExists = sidebarLinks.value.some(
|
||||
@@ -422,10 +421,10 @@ const addContactUsDetails = () => {
|
||||
|
||||
sidebarLinks.value.push({
|
||||
label: 'Contact Us',
|
||||
icon: settingsStore.contactUsURL?.data ? 'Headset' : 'Mail',
|
||||
to: settingsStore.contactUsURL?.data
|
||||
? settingsStore.contactUsURL.data
|
||||
: settingsStore.contactUsEmail?.data,
|
||||
icon: settings.data?.contact_us_url ? 'Headset' : 'Mail',
|
||||
to: settings.data?.contact_us_url
|
||||
? settings.data?.contact_us_url
|
||||
: settings.data?.contact_us_email,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -559,8 +558,8 @@ const steps = reactive([
|
||||
completed: false,
|
||||
onClick: () => {
|
||||
minimize.value = true
|
||||
settingsStore.activeTab = 'Members'
|
||||
settingsStore.isSettingsOpen = true
|
||||
activeTab.value = 'Members'
|
||||
isSettingsOpen.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -171,7 +171,7 @@ const showQuizLoader = ref(false)
|
||||
const quizLoadTimer = ref(0)
|
||||
const currentQuiz = ref(null)
|
||||
const nextQuiz = ref({})
|
||||
const { preventSkippingVideos } = useSettings()
|
||||
const { settings } = useSettings()
|
||||
|
||||
const props = defineProps({
|
||||
file: {
|
||||
@@ -299,7 +299,7 @@ const toggleMute = () => {
|
||||
|
||||
const changeCurrentTime = () => {
|
||||
if (
|
||||
preventSkippingVideos.data &&
|
||||
settings.data?.prevent_skipping_videos &&
|
||||
currentTime.value > videoRef.value.currentTime
|
||||
)
|
||||
return
|
||||
|
||||
@@ -340,11 +340,12 @@ import { sessionStore } from '../stores/session'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import {
|
||||
openSettings,
|
||||
escapeHTML,
|
||||
getMetaInfo,
|
||||
openSettings,
|
||||
sanitizeHTML,
|
||||
updateMetaInfo,
|
||||
validateFile,
|
||||
escapeHTML,
|
||||
} from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -502,6 +503,9 @@ const imageResource = createResource({
|
||||
})
|
||||
|
||||
const validateFields = () => {
|
||||
batch.description = sanitizeHTML(batch.description)
|
||||
batch.batch_details = sanitizeHTML(batch.batch_details)
|
||||
|
||||
Object.keys(batch).forEach((key) => {
|
||||
if (
|
||||
!['description', 'batch_details'].includes(key) &&
|
||||
|
||||
@@ -353,11 +353,12 @@ import { capture, startRecording, stopRecording } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import {
|
||||
openSettings,
|
||||
escapeHTML,
|
||||
getMetaInfo,
|
||||
openSettings,
|
||||
sanitizeHTML,
|
||||
updateMetaInfo,
|
||||
validateFile,
|
||||
escapeHTML,
|
||||
} from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
@@ -539,6 +540,8 @@ const imageResource = createResource({
|
||||
})
|
||||
|
||||
const validateFields = () => {
|
||||
course.description = sanitizeHTML(course.description)
|
||||
|
||||
Object.keys(course).forEach((key) => {
|
||||
if (key != 'description' && typeof course[key] === 'string') {
|
||||
course[key] = escapeHTML(course[key])
|
||||
|
||||
@@ -158,7 +158,7 @@ import { computed, onMounted, reactive, inject } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { escapeHTML, getFileSize, validateFile } from '@/utils'
|
||||
import { escapeHTML, getFileSize, sanitizeHTML, validateFile } from '@/utils'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
@@ -254,8 +254,18 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
if (props.jobName != 'new') jobDetail.reload()
|
||||
addKeyboardShortcuts()
|
||||
})
|
||||
|
||||
const addKeyboardShortcuts = () => {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
|
||||
e.preventDefault()
|
||||
saveJob()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveJob = () => {
|
||||
validateJobFields()
|
||||
if (jobDetail.data) {
|
||||
@@ -304,6 +314,7 @@ const editJobDetails = () => {
|
||||
}
|
||||
|
||||
const validateJobFields = () => {
|
||||
job.description = sanitizeHTML(job.description)
|
||||
Object.keys(job).forEach((key) => {
|
||||
if (key != 'description' && typeof job[key] === 'string') {
|
||||
job[key] = escapeHTML(job[key])
|
||||
@@ -359,7 +370,7 @@ const breadcrumbs = computed(() => {
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: props.jobName == 'new' ? 'New Job' : jobDetail.data?.title,
|
||||
title: props.jobName == 'new' ? 'New Job' : jobDetail.data?.job_title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -168,7 +168,7 @@ const testCaseSection = ref<HTMLElement | null>(null)
|
||||
const testCases = ref<TestCase[]>([])
|
||||
const boilerplate = ref<string>('')
|
||||
const { brand } = sessionStore()
|
||||
const { livecodeURL } = useSettings()
|
||||
const { settings } = useSettings()
|
||||
const router = useRouter()
|
||||
const fromLesson = ref(false)
|
||||
const falconURL = ref<string>('https://falcon.frappe.io/')
|
||||
@@ -291,8 +291,8 @@ watch(
|
||||
)
|
||||
|
||||
const loadFalcon = () => {
|
||||
if (livecodeURL.data) {
|
||||
falconURL.value = livecodeURL.data
|
||||
if (settings.data) {
|
||||
falconURL.value = settings.data.livecode_url
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
|
||||
@@ -275,7 +275,7 @@ let router = createRouter({
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const { userResource } = usersStore()
|
||||
let { isLoggedIn } = sessionStore()
|
||||
const { allowGuestAccess } = useSettings()
|
||||
const { settings } = useSettings()
|
||||
|
||||
try {
|
||||
if (isLoggedIn) {
|
||||
@@ -288,8 +288,8 @@ router.beforeEach(async (to, from, next) => {
|
||||
if (!isLoggedIn) {
|
||||
if (to.name == 'Home') router.push({ name: 'Courses' })
|
||||
|
||||
await allowGuestAccess.promise
|
||||
if (!allowGuestAccess.data) {
|
||||
await settings.promise
|
||||
if (!settings.data.allow_guest_access) {
|
||||
window.location.href = '/login'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,32 +8,10 @@ export const useSettings = defineStore('settings', () => {
|
||||
const isCommandPaletteOpen = ref(false)
|
||||
const activeTab = ref(null)
|
||||
|
||||
const allowGuestAccess = createResource({
|
||||
url: 'lms.lms.api.get_lms_setting',
|
||||
params: { field: 'allow_guest_access' },
|
||||
const settings = createResource({
|
||||
url: 'lms.lms.api.get_lms_settings',
|
||||
auto: true,
|
||||
cache: ['allowGuestAccess'],
|
||||
})
|
||||
|
||||
const preventSkippingVideos = createResource({
|
||||
url: 'lms.lms.api.get_lms_setting',
|
||||
params: { field: 'prevent_skipping_videos' },
|
||||
auto: true,
|
||||
cache: ['preventSkippingVideos'],
|
||||
})
|
||||
|
||||
const contactUsEmail = createResource({
|
||||
url: 'lms.lms.api.get_lms_setting',
|
||||
params: { field: 'contact_us_email' },
|
||||
auto: true,
|
||||
cache: ['contactUsEmail'],
|
||||
})
|
||||
|
||||
const contactUsURL = createResource({
|
||||
url: 'lms.lms.api.get_lms_setting',
|
||||
params: { field: 'contact_us_url' },
|
||||
auto: true,
|
||||
cache: ['contactUsURL'],
|
||||
cache: 'LMS Settings',
|
||||
})
|
||||
|
||||
const sidebarSettings = createResource({
|
||||
@@ -42,22 +20,11 @@ export const useSettings = defineStore('settings', () => {
|
||||
auto: false,
|
||||
})
|
||||
|
||||
const livecodeURL = createResource({
|
||||
url: 'lms.lms.api.get_lms_setting',
|
||||
params: { field: 'livecode_url' },
|
||||
auto: true,
|
||||
cache: ['livecodeURL'],
|
||||
})
|
||||
|
||||
return {
|
||||
isSettingsOpen,
|
||||
isCommandPaletteOpen,
|
||||
activeTab,
|
||||
allowGuestAccess,
|
||||
preventSkippingVideos,
|
||||
contactUsEmail,
|
||||
contactUsURL,
|
||||
settings,
|
||||
sidebarSettings,
|
||||
livecodeURL,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import SimpleImage from '@editorjs/simple-image'
|
||||
import Table from '@editorjs/table'
|
||||
import Plyr from 'plyr'
|
||||
import 'plyr/dist/plyr.css'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
@@ -545,6 +546,26 @@ export const escapeHTML = (text) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const sanitizeHTML = (text) => {
|
||||
text = DOMPurify.sanitize(decodeEntities(text), {
|
||||
ALLOWED_TAGS: [
|
||||
'b',
|
||||
'i',
|
||||
'em',
|
||||
'strong',
|
||||
'a',
|
||||
'p',
|
||||
'br',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'img',
|
||||
],
|
||||
ALLOWED_ATTR: ['href', 'target', 'src'],
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
export const canCreateCourse = () => {
|
||||
const { userResource } = usersStore()
|
||||
return (
|
||||
@@ -596,7 +617,7 @@ const setupPlyrForVideo = (video, players) => {
|
||||
const current_time = player.currentTime
|
||||
const newTime = getTargetTime(player, e)
|
||||
if (
|
||||
useSettings().preventSkippingVideos.data &&
|
||||
useSettings().settings.data?.prevent_skipping_videos &&
|
||||
parseFloat(newTime) > current_time
|
||||
) {
|
||||
e.preventDefault()
|
||||
|
||||
Reference in New Issue
Block a user