mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge branch 'develop' of https://github.com/frappe/lms into demo-data
This commit is contained in:
Vendored
+2
@@ -60,6 +60,8 @@ declare module 'vue' {
|
||||
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
|
||||
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
|
||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
||||
GoogleMeetAccountModal: typeof import('./src/components/Settings/GoogleMeetAccountModal.vue')['default']
|
||||
GoogleMeetSettings: typeof import('./src/components/Settings/GoogleMeetSettings.vue')['default']
|
||||
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
|
||||
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
|
||||
InlineLessonMenu: typeof import('./src/components/Notes/InlineLessonMenu.vue')['default']
|
||||
|
||||
@@ -419,11 +419,15 @@ const canGradeSubmission = computed(() => {
|
||||
})
|
||||
|
||||
const canModifyAssignment = computed(() => {
|
||||
return (
|
||||
!submissionResource.doc ||
|
||||
(submissionResource.doc?.owner == user.data?.name &&
|
||||
submissionResource.doc?.status == 'Not Graded')
|
||||
)
|
||||
if (canGradeSubmission.value) {
|
||||
return true
|
||||
} else if (
|
||||
submissionResource.doc?.owner == user.data?.name &&
|
||||
submissionResource.doc?.status == 'Not Graded'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const submissionStatusOptions = computed(() => {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center text-sm font-medium space-x-2">
|
||||
<span>
|
||||
{{ __('What does include in preview mean?') }}
|
||||
{{ __('What are Instructor Notes?') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1 leading-5">
|
||||
{{
|
||||
__(
|
||||
'If Include in Preview is enabled for a lesson then the lesson will also be accessible to non logged in users.'
|
||||
'Instructor Notes are private notes that only instructors can see. They can be used to provide additional context or guidance for the lesson.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="space-y-4 text-base">
|
||||
<FormControl label="Title" v-model="chapter.title" :required="true" />
|
||||
<FormControl
|
||||
label="Title"
|
||||
v-model="chapter.title"
|
||||
:required="true"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Switch
|
||||
size="sm"
|
||||
:label="__('SCORM Package')"
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
/>
|
||||
</div>
|
||||
<FormControl
|
||||
v-if="props.conferencingProvider === 'Zoom'"
|
||||
v-model="liveClass.auto_recording"
|
||||
type="select"
|
||||
:options="getRecordingOptions()"
|
||||
@@ -99,10 +100,9 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
zoomAccount: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
zoomAccount: String,
|
||||
googleMeetAccount: String,
|
||||
conferencingProvider: String,
|
||||
})
|
||||
|
||||
let liveClass = reactive({
|
||||
@@ -159,8 +159,23 @@ const createLiveClass = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const createGoogleMeetLiveClass = createResource({
|
||||
url: 'lms.lms.doctype.lms_batch.lms_batch.create_google_meet_live_class',
|
||||
makeParams(values) {
|
||||
return {
|
||||
batch_name: values.batch,
|
||||
google_meet_account: props.googleMeetAccount,
|
||||
...values,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const submitLiveClass = (close) => {
|
||||
return createLiveClass.submit(liveClass, {
|
||||
const resource =
|
||||
props.conferencingProvider === 'Google Meet'
|
||||
? createGoogleMeetLiveClass
|
||||
: createLiveClass
|
||||
return resource.submit(liveClass, {
|
||||
validate() {
|
||||
validateFormFields()
|
||||
},
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex flex-col h-full text-p-base">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold mb-1 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
<div class="space-y-2">
|
||||
<div class="font-semibold text-xl text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Badge
|
||||
@@ -21,9 +26,6 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ __(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto">
|
||||
<SettingFields :sections="sections" :data="branding.data" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex item-center space-x-2">
|
||||
<Button variant="solid" @click="() => (showForm = !showForm)">
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<template #prefix>
|
||||
<Plus class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title:
|
||||
accountID === 'new'
|
||||
? __('New Google Meet Account')
|
||||
: __('Edit Google Meet Account'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Save'),
|
||||
variant: 'solid',
|
||||
onClick: ({ close }) => {
|
||||
saveAccount(close)
|
||||
},
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="mb-4">
|
||||
<FormControl
|
||||
v-model="account.enabled"
|
||||
:label="__('Enabled')"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="account.name"
|
||||
:label="__('Account Name')"
|
||||
type="text"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
v-model="account.member"
|
||||
:label="__('Member')"
|
||||
doctype="Course Evaluator"
|
||||
:onCreate="(value: string, close: () => void) => openSettings('Members', close)"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
v-model="account.google_calendar"
|
||||
:label="__('Google Calendar')"
|
||||
doctype="Google Calendar"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { call, Dialog, FormControl, toast } from 'frappe-ui'
|
||||
import { inject, reactive, watch } from 'vue'
|
||||
import { User } from '@/components/Settings/types'
|
||||
import { openSettings, cleanError } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { useTelemetry } from 'frappe-ui/frappe'
|
||||
|
||||
interface GoogleMeetAccount {
|
||||
name: string
|
||||
account_name: string
|
||||
enabled: boolean
|
||||
member: string
|
||||
google_calendar: string
|
||||
}
|
||||
|
||||
interface GoogleMeetAccounts {
|
||||
data: GoogleMeetAccount[]
|
||||
reload: () => void
|
||||
insert: {
|
||||
submit: (
|
||||
data: GoogleMeetAccount,
|
||||
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||
) => void
|
||||
}
|
||||
setValue: {
|
||||
submit: (
|
||||
data: GoogleMeetAccount,
|
||||
options: { onSuccess: () => void; onError: (err: any) => void }
|
||||
) => void
|
||||
}
|
||||
}
|
||||
|
||||
const show = defineModel('show')
|
||||
const user = inject<User | null>('$user')
|
||||
const googleMeetAccounts = defineModel<GoogleMeetAccounts>('googleMeetAccounts')
|
||||
const { capture } = useTelemetry()
|
||||
|
||||
const account = reactive({
|
||||
name: '',
|
||||
enabled: false,
|
||||
member: user?.data?.name || '',
|
||||
google_calendar: '',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
accountID: {
|
||||
type: String,
|
||||
default: 'new',
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.accountID,
|
||||
(val) => {
|
||||
if (val === 'new') {
|
||||
account.name = ''
|
||||
account.enabled = false
|
||||
account.member = user?.data?.name || ''
|
||||
account.google_calendar = ''
|
||||
} else if (val && val !== 'new') {
|
||||
const acc = googleMeetAccounts.value?.data.find((acc) => acc.name === val)
|
||||
if (acc) {
|
||||
account.name = acc.name
|
||||
account.enabled = acc.enabled || false
|
||||
account.member = acc.member
|
||||
account.google_calendar = acc.google_calendar
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const saveAccount = (close: () => void) => {
|
||||
if (props.accountID == 'new') {
|
||||
createAccount(close)
|
||||
} else {
|
||||
updateAccount(close)
|
||||
}
|
||||
}
|
||||
|
||||
const createAccount = (close: () => void) => {
|
||||
googleMeetAccounts.value?.insert.submit(
|
||||
{
|
||||
account_name: account.name,
|
||||
...account,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
capture('google_meet_account_linked')
|
||||
googleMeetAccounts.value?.reload()
|
||||
close()
|
||||
toast.success(__('Google Meet Account created successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
console.error(err)
|
||||
close()
|
||||
toast.error(
|
||||
cleanError(err.messages[0]) ||
|
||||
__('Error creating Google Meet Account')
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updateAccount = async (close: () => void) => {
|
||||
if (props.accountID != account.name) {
|
||||
await renameDoc()
|
||||
}
|
||||
setValue(close)
|
||||
}
|
||||
|
||||
const renameDoc = async () => {
|
||||
await call('frappe.client.rename_doc', {
|
||||
doctype: 'LMS Google Meet Settings',
|
||||
old_name: props.accountID,
|
||||
new_name: account.name,
|
||||
})
|
||||
}
|
||||
|
||||
const setValue = (close: () => void) => {
|
||||
googleMeetAccounts.value?.setValue.submit(
|
||||
{
|
||||
...account,
|
||||
name: account.name,
|
||||
account_name: props.accountID,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
googleMeetAccounts.value?.reload()
|
||||
close()
|
||||
toast.success(__('Google Meet Account updated successfully'))
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error(err)
|
||||
close()
|
||||
toast.error(
|
||||
cleanError(err.messages[0]) ||
|
||||
__('Error updating Google Meet Account')
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="flex flex-col min-h-0 text-base">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="text-xl font-semibold text-ink-gray-9">
|
||||
{{ label }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-5">
|
||||
<Button @click="openForm('new')">
|
||||
<template #prefix>
|
||||
<Plus class="h-3 w-3 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="googleMeetAccounts.data?.length" class="overflow-y-scroll">
|
||||
<ListView
|
||||
:columns="columns"
|
||||
:rows="googleMeetAccounts.data"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
onRowClick: (row) => {
|
||||
openForm(row.name)
|
||||
},
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in columns">
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
class="h-4 w-4 stroke-1.5"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in googleMeetAccounts.data">
|
||||
<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 v-if="column.key == 'enabled'">
|
||||
<Badge v-if="row[column.key]" theme="green">
|
||||
{{ __('Enabled') }}
|
||||
</Badge>
|
||||
<Badge v-else theme="gray">
|
||||
{{ __('Disabled') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-else class="leading-5 text-sm">
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="removeAccount(selections, unselectAll)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
<GoogleMeetAccountModal
|
||||
v-model="showForm"
|
||||
v-model:googleMeetAccounts="googleMeetAccounts"
|
||||
:accountID="currentAccount"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Badge,
|
||||
call,
|
||||
createListResource,
|
||||
FeatherIcon,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { cleanError } from '@/utils'
|
||||
import { User } from '@/components/Settings/types'
|
||||
import GoogleMeetAccountModal from '@/components/Settings/GoogleMeetAccountModal.vue'
|
||||
|
||||
const user = inject<User | null>('$user')
|
||||
const showForm = ref(false)
|
||||
const currentAccount = ref<string | null>(null)
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
description: String,
|
||||
})
|
||||
|
||||
const googleMeetAccounts = createListResource({
|
||||
doctype: 'LMS Google Meet Settings',
|
||||
fields: [
|
||||
'name',
|
||||
'enabled',
|
||||
'member',
|
||||
'member_name',
|
||||
'member_image',
|
||||
'google_calendar',
|
||||
],
|
||||
cache: ['googleMeetAccounts'],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchGoogleMeetAccounts()
|
||||
})
|
||||
|
||||
const fetchGoogleMeetAccounts = () => {
|
||||
if (!user?.data?.is_moderator && !user?.data?.is_evaluator) return
|
||||
|
||||
if (!user?.data?.is_moderator) {
|
||||
googleMeetAccounts.update({
|
||||
filters: {
|
||||
member: user.data.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
googleMeetAccounts.reload()
|
||||
}
|
||||
|
||||
const openForm = (accountID: string) => {
|
||||
currentAccount.value = accountID
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
const removeAccount = (selections, unselectAll) => {
|
||||
call('lms.lms.api.delete_documents', {
|
||||
doctype: 'LMS Google Meet Settings',
|
||||
documents: Array.from(selections),
|
||||
})
|
||||
.then(() => {
|
||||
googleMeetAccounts.reload()
|
||||
toast.success(__('Google Meet Account deleted successfully'))
|
||||
unselectAll()
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
cleanError(err.messages[0]) || __('Error deleting Google Meet Account')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Member'),
|
||||
key: 'member_name',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
label: __('Account Name'),
|
||||
key: 'name',
|
||||
icon: 'video',
|
||||
},
|
||||
{
|
||||
label: __('Status'),
|
||||
key: 'enabled',
|
||||
align: 'center',
|
||||
icon: 'check-square',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex item-center space-x-2">
|
||||
<Button variant="solid" @click="() => (showForm = !showForm)">
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<template #prefix>
|
||||
<Plus class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
|
||||
@@ -131,7 +131,7 @@ watch(newGateway, () => {
|
||||
let fields = gatewayFields.data || []
|
||||
arrangeFields(fields)
|
||||
newGatewayFields.value = makeSections(fields)
|
||||
prepareGatewayData()
|
||||
prepareGatewayData(fields)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -209,13 +209,11 @@ const allGatewayOptions = computed(() => {
|
||||
return options.map((gateway: string) => ({ label: gateway, value: gateway }))
|
||||
})
|
||||
|
||||
const prepareGatewayData = () => {
|
||||
const prepareGatewayData = (fields: any[]) => {
|
||||
newGatewayData.value = {}
|
||||
if (newGatewayFields.value.length) {
|
||||
newGatewayFields.value.forEach((field: any) => {
|
||||
newGatewayData.value[field.fieldname] = field.default || ''
|
||||
})
|
||||
}
|
||||
fields.forEach((field: any) => {
|
||||
newGatewayData.value[field.name] = field.default || ''
|
||||
})
|
||||
}
|
||||
|
||||
const makeSections = (fields: any[]) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
@@ -88,6 +88,7 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
FeatherIcon,
|
||||
ListView,
|
||||
@@ -97,10 +98,12 @@ import {
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import PaymentGatewayDetails from '@/components/Settings/PaymentGatewayDetails.vue'
|
||||
import { cleanError } from '@/utils'
|
||||
|
||||
const showForm = ref(false)
|
||||
const currentGateway = ref(null)
|
||||
@@ -128,6 +131,23 @@ const openForm = (gatewayID) => {
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
const removeAccount = (selections, unselectAll) => {
|
||||
call('lms.lms.api.delete_documents', {
|
||||
doctype: 'Payment Gateway',
|
||||
documents: Array.from(selections),
|
||||
})
|
||||
.then(() => {
|
||||
paymentGateways.reload()
|
||||
toast.success(__('Payment gateways deleted successfully'))
|
||||
unselectAll()
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
cleanError(err.messages[0]) || __('Error deleting payment gateways')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
<div class="flex flex-col h-full text-base overflow-y-hidden">
|
||||
<div class="">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="text-xl font-semibold leading-none text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Badge
|
||||
@@ -19,9 +22,6 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingFields :sections="sections" :data="data.doc" />
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
:doctype="field.doctype"
|
||||
:label="__(field.label)"
|
||||
:description="__(field.description)"
|
||||
:required="field.reqd"
|
||||
/>
|
||||
|
||||
<div v-else-if="field.type == 'Code'">
|
||||
@@ -115,6 +116,7 @@
|
||||
:rows="field.rows"
|
||||
:options="field.options"
|
||||
:description="field.description"
|
||||
:required="field.reqd"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,7 @@ import PaymentGateways from '@/components/Settings/PaymentGateways.vue'
|
||||
import Coupons from '@/components/Settings/Coupons/Coupons.vue'
|
||||
import Transactions from '@/components/Settings/Transactions/Transactions.vue'
|
||||
import ZoomSettings from '@/components/Settings/ZoomSettings.vue'
|
||||
import GoogleMeetSettings from '@/components/Settings/GoogleMeetSettings.vue'
|
||||
import Badges from '@/components/Settings/Badges.vue'
|
||||
|
||||
const show = defineModel()
|
||||
@@ -268,34 +269,6 @@ const tabsStructure = computed(() => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Lists',
|
||||
hideLabel: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description:
|
||||
'Add new members or manage roles and permissions of existing members',
|
||||
icon: 'UserRoundPlus',
|
||||
template: markRaw(Members),
|
||||
},
|
||||
{
|
||||
label: 'Evaluators',
|
||||
description: '',
|
||||
icon: 'UserCheck',
|
||||
description:
|
||||
'Add new evaluators or check the slots existing evaluators',
|
||||
template: markRaw(Evaluators),
|
||||
},
|
||||
{
|
||||
label: 'Zoom Accounts',
|
||||
description:
|
||||
'Manage zoom accounts to conduct live classes from batches',
|
||||
icon: 'Video',
|
||||
template: markRaw(ZoomSettings),
|
||||
},
|
||||
{
|
||||
label: 'Badges',
|
||||
description:
|
||||
@@ -317,6 +290,27 @@ const tabsStructure = computed(() => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Users',
|
||||
hideLabel: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description:
|
||||
'Add new members or manage roles and permissions of existing members',
|
||||
icon: 'User',
|
||||
template: markRaw(Members),
|
||||
},
|
||||
{
|
||||
label: 'Evaluators',
|
||||
description: '',
|
||||
icon: 'UserCircle2',
|
||||
description:
|
||||
'Add new evaluators or check the slots of existing evaluators',
|
||||
template: markRaw(Evaluators),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Payment',
|
||||
hideLabel: false,
|
||||
@@ -387,6 +381,26 @@ const tabsStructure = computed(() => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Conferencing',
|
||||
hideLabel: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Zoom',
|
||||
description:
|
||||
'Manage zoom accounts to conduct live classes from batches',
|
||||
icon: 'Video',
|
||||
template: markRaw(ZoomSettings),
|
||||
},
|
||||
{
|
||||
label: 'Google Meet',
|
||||
description:
|
||||
'Manage Google Meet accounts to conduct live classes from batches',
|
||||
icon: 'Presentation',
|
||||
template: markRaw(GoogleMeetSettings),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Customize',
|
||||
hideLabel: false,
|
||||
@@ -394,6 +408,8 @@ const tabsStructure = computed(() => {
|
||||
{
|
||||
label: 'Branding',
|
||||
icon: 'Blocks',
|
||||
description:
|
||||
'Customize the brand name and logo to make the application your own',
|
||||
template: markRaw(BrandSettings),
|
||||
sections: [
|
||||
{
|
||||
@@ -482,6 +498,8 @@ const tabsStructure = computed(() => {
|
||||
{
|
||||
label: 'Signup',
|
||||
icon: 'LogIn',
|
||||
description:
|
||||
'Manage the settings related to user signup and registration',
|
||||
sections: [
|
||||
{
|
||||
columns: [
|
||||
@@ -517,6 +535,8 @@ const tabsStructure = computed(() => {
|
||||
{
|
||||
label: 'SEO',
|
||||
icon: 'Search',
|
||||
description:
|
||||
'Manage the SEO settings to improve your website ranking on search engines',
|
||||
sections: [
|
||||
{
|
||||
columns: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
<div class="text-xl font-semibold mb-2 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
|
||||
@@ -165,24 +165,48 @@
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<Link
|
||||
doctype="LMS Zoom Settings"
|
||||
:label="__('Zoom Account')"
|
||||
v-model="batchDetail.doc.zoom_account"
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
openSettings('Zoom Accounts', close)
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Uploader
|
||||
v-model="batchDetail.doc.video_link"
|
||||
:label="__('Preview Video')"
|
||||
type="video"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
<Uploader
|
||||
v-model="batchDetail.doc.video_link"
|
||||
:label="__('Preview Video')"
|
||||
type="video"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 pb-5 space-y-5 border-b mb-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||
{{ __('Conferencing') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="batchDetail.doc.conferencing_provider"
|
||||
type="select"
|
||||
:options="conferencingOptions"
|
||||
:label="__('Conferencing Provider')"
|
||||
/>
|
||||
<Link
|
||||
v-if="batchDetail.doc.conferencing_provider === 'Zoom'"
|
||||
doctype="LMS Zoom Settings"
|
||||
:label="__('Zoom Account')"
|
||||
v-model="batchDetail.doc.zoom_account"
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
openSettings('Zoom Accounts', close)
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Link
|
||||
v-if="batchDetail.doc.conferencing_provider === 'Google Meet'"
|
||||
doctype="LMS Google Meet Settings"
|
||||
:label="__('Google Meet Account')"
|
||||
v-model="batchDetail.doc.google_meet_account"
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
openSettings('Google Meet Accounts', close)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -463,14 +487,31 @@ const trashBatch = (close) => {
|
||||
})
|
||||
}
|
||||
|
||||
const conferencingOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: '',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: __('Zoom'),
|
||||
value: 'Zoom',
|
||||
},
|
||||
{
|
||||
label: __('Google Meet'),
|
||||
value: 'Google Meet',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const mediumOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Online',
|
||||
label: __('Online'),
|
||||
value: 'Online',
|
||||
},
|
||||
{
|
||||
label: 'Offline',
|
||||
label: __('Offline'),
|
||||
value: 'Offline',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div
|
||||
v-if="isAdmin() && !batch.data?.zoom_account"
|
||||
v-if="isAdmin() && !hasProviderAccount()"
|
||||
class="flex lg:items-center space-x-2 mb-5 bg-surface-amber-1 px-3 py-2 rounded-lg text-ink-amber-3"
|
||||
>
|
||||
<AlertCircle class="size-7 md:size-4 stroke-1.5" />
|
||||
<span class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'Link a Zoom account to this batch from the Settings tab to create live classes'
|
||||
'Please select a conferencing provider and add an account to the batch to create live classes.'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
@@ -64,12 +64,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="canAccessClass(cls)"
|
||||
v-if="canAccessClass(cls) && cls.join_url"
|
||||
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
||||
>
|
||||
<a
|
||||
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
||||
:href="cls.start_url"
|
||||
:href="cls.start_url || cls.join_url"
|
||||
target="_blank"
|
||||
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
||||
:class="cls.join_url ? 'w-full' : 'w-1/2'"
|
||||
@@ -111,6 +111,8 @@
|
||||
v-model="showLiveClassModal"
|
||||
:batch="batch.data?.name"
|
||||
:zoomAccount="batch.data?.zoom_account"
|
||||
:googleMeetAccount="batch.data?.google_meet_account"
|
||||
:conferencingProvider="batch.data?.conferencing_provider"
|
||||
v-model:reloadLiveClasses="liveClasses"
|
||||
/>
|
||||
|
||||
@@ -165,6 +167,8 @@ const liveClasses = createListResource({
|
||||
'start_url',
|
||||
'join_url',
|
||||
'owner',
|
||||
'conferencing_provider',
|
||||
'batch_name',
|
||||
],
|
||||
orderBy: 'date',
|
||||
auto: true,
|
||||
@@ -174,9 +178,20 @@ const openLiveClassModal = () => {
|
||||
showLiveClassModal.value = true
|
||||
}
|
||||
|
||||
const hasProviderAccount = () => {
|
||||
const data = props.batch.data
|
||||
if (data?.conferencing_provider === 'Zoom' && data?.zoom_account) return true
|
||||
if (
|
||||
data?.conferencing_provider === 'Google Meet' &&
|
||||
data?.google_meet_account
|
||||
)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
const canCreateClass = () => {
|
||||
if (readOnlyMode) return false
|
||||
if (!props.batch.data?.zoom_account) return false
|
||||
if (!hasProviderAccount()) return false
|
||||
return isAdmin()
|
||||
}
|
||||
|
||||
@@ -209,8 +224,8 @@ const hasClassEnded = (cls) => {
|
||||
const openAttendanceModal = (cls) => {
|
||||
if (!isAdmin()) return
|
||||
if (cls.attendees <= 0) return
|
||||
showAttendance.value = true
|
||||
attendanceFor.value = cls
|
||||
showAttendance.value = true
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@@ -8,81 +8,86 @@
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="text-base">
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
:label="__('Start Date')"
|
||||
type="date"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_date"
|
||||
:label="__('End Date')"
|
||||
type="date"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
v-model="batch.category"
|
||||
:label="__('Category')"
|
||||
:allowCreate="true"
|
||||
:onCreate="
|
||||
() => {
|
||||
openSettings('Categories')
|
||||
show = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-model="batch.start_time"
|
||||
:label="__('Start Time')"
|
||||
type="time"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_time"
|
||||
:label="__('End Time')"
|
||||
type="time"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.timezone"
|
||||
:label="__('Timezone')"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.seat_count"
|
||||
:label="__('Seat Count')"
|
||||
type="number"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
<FormControl
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
:label="__('Start Date')"
|
||||
type="date"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_date"
|
||||
:label="__('End Date')"
|
||||
type="date"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.start_time"
|
||||
:label="__('Start Time')"
|
||||
type="time"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_time"
|
||||
:label="__('End Time')"
|
||||
type="time"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.timezone"
|
||||
:label="__('Timezone')"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
v-model="batch.category"
|
||||
:label="__('Category')"
|
||||
:allowCreate="true"
|
||||
:onCreate="
|
||||
() => {
|
||||
openSettings('Categories')
|
||||
show = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.seat_count"
|
||||
:label="__('Seat Count')"
|
||||
type="number"
|
||||
:required="false"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.medium"
|
||||
type="select"
|
||||
:options="mediumOptions"
|
||||
:label="__('Medium')"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5 border-t mt-5 pt-5">
|
||||
<MultiSelect
|
||||
v-model="batch.instructors"
|
||||
doctype="Course Evaluator"
|
||||
:label="__('Instructors')"
|
||||
:required="true"
|
||||
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.description"
|
||||
:label="__('Description')"
|
||||
type="textarea"
|
||||
:required="true"
|
||||
:rows="4"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<MultiSelect
|
||||
v-model="batch.instructors"
|
||||
doctype="Course Evaluator"
|
||||
:label="__('Instructors')"
|
||||
:required="true"
|
||||
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.description"
|
||||
:label="__('Description')"
|
||||
type="textarea"
|
||||
:required="true"
|
||||
:rows="4"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Batch Details') }}
|
||||
@@ -93,7 +98,7 @@
|
||||
@change="(val: string) => (batch.batch_details = 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]"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[14rem] overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +116,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
||||
import { ref, inject, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { cleanError, openSettings, sanitizeHTML, escapeHTML } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
@@ -139,6 +144,7 @@ type Batch = {
|
||||
instructors: string[]
|
||||
category: string | null
|
||||
seat_count: number
|
||||
medium: string | null
|
||||
}
|
||||
|
||||
const batch = ref<Batch>({
|
||||
@@ -153,6 +159,7 @@ const batch = ref<Batch>({
|
||||
instructors: [],
|
||||
category: null,
|
||||
seat_count: 0,
|
||||
medium: null,
|
||||
})
|
||||
|
||||
const validateFields = () => {
|
||||
@@ -227,4 +234,17 @@ onBeforeUnmount(() => {
|
||||
data: batch.value,
|
||||
})
|
||||
})
|
||||
|
||||
const mediumOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Online'),
|
||||
value: 'Online',
|
||||
},
|
||||
{
|
||||
label: __('Offline'),
|
||||
value: 'Offline',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -191,6 +191,26 @@
|
||||
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
||||
{{ __('Pricing and Certification') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.paid_course"
|
||||
:label="__('Paid Course')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.enable_certification"
|
||||
:label="__('Completion Certificate')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.paid_certificate"
|
||||
:label="__('Paid Certificate')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div
|
||||
v-if="
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
v-model="course.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
@@ -57,7 +58,7 @@
|
||||
@change="(val: string) => (course.description = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem]"
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[17rem] overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,6 +88,7 @@ const router = useRouter()
|
||||
const { capture } = useTelemetry()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
const user = inject<any>('$user')
|
||||
const courseCreated = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
courses: any
|
||||
@@ -139,6 +141,7 @@ const saveCourse = (close: () => void = () => {}) => {
|
||||
toast.success(__('Course created successfully'))
|
||||
close()
|
||||
capture('course_created')
|
||||
courseCreated.value = true
|
||||
router.push({
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: data.name },
|
||||
@@ -178,8 +181,10 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keyboardShortcut)
|
||||
capture('course_form_closed', {
|
||||
data: course.value,
|
||||
})
|
||||
if (!courseCreated.value) {
|
||||
capture('course_form_closed', {
|
||||
data: course.value,
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,17 +15,22 @@
|
||||
</Button>
|
||||
</header>
|
||||
<div class="py-5">
|
||||
<div class="w-5/6 mx-auto">
|
||||
<div class="grid grid-cols-2 gap-5 w-5/6 mx-auto">
|
||||
<FormControl
|
||||
v-model="lesson.title"
|
||||
label="Title"
|
||||
:label="__('Title')"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<FormControl
|
||||
<Switch
|
||||
v-model="lesson.include_in_preview"
|
||||
type="checkbox"
|
||||
label="Include in Preview"
|
||||
:label="__('Include in Preview')"
|
||||
:description="
|
||||
__(
|
||||
'If enabled, the lesson will also be accessible to users who are not enrolled in the course.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-t mt-4">
|
||||
@@ -83,6 +88,7 @@ import {
|
||||
Button,
|
||||
createResource,
|
||||
FormControl,
|
||||
Switch,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
@@ -708,8 +714,8 @@ iframe {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.ce-popover--opened > .ce-popover__container {
|
||||
max-height: unset;
|
||||
.ce-popover--opened {
|
||||
max-height: unset !important;
|
||||
}
|
||||
|
||||
.cdx-search-field__icon svg {
|
||||
|
||||
@@ -126,6 +126,7 @@ export function getEditorTools() {
|
||||
defaultStyle: 'ordered',
|
||||
},
|
||||
},
|
||||
upload: Upload,
|
||||
table: {
|
||||
class: Table,
|
||||
inlineToolbar: true,
|
||||
@@ -133,7 +134,6 @@ export function getEditorTools() {
|
||||
quiz: Quiz,
|
||||
assignment: Assignment,
|
||||
program: Program,
|
||||
upload: Upload,
|
||||
markdown: {
|
||||
class: Markdown,
|
||||
inlineToolbar: true,
|
||||
|
||||
Reference in New Issue
Block a user