feat: settings
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ node_modules
|
||||
package-lock.json
|
||||
lms/public/frontend
|
||||
lms/www/lms.html
|
||||
frappe-ui
|
||||
@@ -35,7 +35,6 @@ bench new-site lms.localhost \
|
||||
bench --site lms.localhost install-app lms
|
||||
bench --site lms.localhost set-config developer_mode 1
|
||||
bench --site lms.localhost clear-cache
|
||||
bench --site lms.localhost set-config mute_emails 1
|
||||
bench use lms.localhost
|
||||
|
||||
bench start
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="flex flex-col overflow-hidden"
|
||||
:class="isSidebarCollapsed ? 'items-center' : ''"
|
||||
>
|
||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
||||
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
||||
<div class="flex flex-col" v-if="sidebarSettings.data">
|
||||
<SidebarLink
|
||||
v-for="link in sidebarLinks"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
|
||||
214
frontend/src/components/Fields.vue
Normal file
214
frontend/src/components/Fields.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="section in sections"
|
||||
:key="section.label"
|
||||
class="first:border-t-0 first:pt-0"
|
||||
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
||||
>
|
||||
<div
|
||||
v-if="!section.hideLabel"
|
||||
class="flex h-7 mb-3 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
||||
>
|
||||
{{ section.label }}
|
||||
</div>
|
||||
<div
|
||||
class="grid gap-4"
|
||||
:class="
|
||||
section.columns
|
||||
? 'grid-cols-' + section.columns
|
||||
: 'grid-cols-2 sm:grid-cols-3'
|
||||
"
|
||||
>
|
||||
<div v-for="field in section.fields" :key="field.name">
|
||||
<div
|
||||
v-if="
|
||||
field.type == 'Check' ||
|
||||
(field.read_only && data[field.name]) ||
|
||||
!field.read_only ||
|
||||
!field.hidden
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="field.type != 'Check'"
|
||||
class="mb-2 text-sm text-gray-600"
|
||||
>
|
||||
{{ __(field.label) }}
|
||||
<span class="text-red-500" v-if="field.mandatory">*</span>
|
||||
</div>
|
||||
<FormControl
|
||||
v-if="field.read_only && field.type !== 'Check'"
|
||||
type="text"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
v-model="data[field.name]"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'Select'"
|
||||
type="select"
|
||||
class="form-control"
|
||||
:class="field.prefix ? 'prefix' : ''"
|
||||
:options="field.options"
|
||||
v-model="data[field.name]"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
>
|
||||
<template v-if="field.prefix" #prefix>
|
||||
<IndicatorIcon :class="field.prefix" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<div
|
||||
v-else-if="field.type == 'Check'"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<FormControl
|
||||
class="form-control"
|
||||
type="checkbox"
|
||||
v-model="data[field.name]"
|
||||
@change="(e) => (data[field.name] = e.target.checked)"
|
||||
:disabled="Boolean(field.read_only)"
|
||||
/>
|
||||
<label
|
||||
class="text-sm text-gray-600"
|
||||
@click="data[field.name] = !data[field.name]"
|
||||
>
|
||||
{{ __(field.label) }}
|
||||
<span class="text-red-500" v-if="field.mandatory">*</span>
|
||||
</label>
|
||||
</div>
|
||||
<Link
|
||||
v-else-if="field.type === 'Link'"
|
||||
class="form-control"
|
||||
:value="data[field.name]"
|
||||
:doctype="field.options"
|
||||
@change="(v) => (data[field.name] = v)"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
:onCreate="field.create"
|
||||
/>
|
||||
<Link
|
||||
v-else-if="field.type === 'User'"
|
||||
class="form-control"
|
||||
:value="getUser(data[field.name]).full_name"
|
||||
:doctype="field.options"
|
||||
@change="(v) => (data[field.name] = v)"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
:hideMe="true"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserAvatar class="mr-2" :user="data[field.name]" size="sm" />
|
||||
</template>
|
||||
<template #item-prefix="{ option }">
|
||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
<Tooltip :text="option.value">
|
||||
<div class="cursor-pointer">
|
||||
{{ getUser(option.value).full_name }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Link>
|
||||
<div v-else-if="field.type === 'Dropdown'">
|
||||
<NestedPopover>
|
||||
<template #target="{ open }">
|
||||
<Button
|
||||
:label="data[field.name]"
|
||||
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-gray-100 px-2 py-1.5 text-base text-gray-800 placeholder-gray-500 transition-colors hover:border-gray-200 hover:bg-gray-200 focus:border-gray-500 focus:bg-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400"
|
||||
>
|
||||
<div class="truncate">{{ data[field.name] }}</div>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4 text-gray-600"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<template #body>
|
||||
<div
|
||||
class="my-2 space-y-1.5 divide-y rounded-lg border border-gray-100 bg-white p-1.5 shadow-xl"
|
||||
>
|
||||
<div>
|
||||
<DropdownItem
|
||||
v-if="field.options?.length"
|
||||
v-for="option in field.options"
|
||||
:key="option.name"
|
||||
:option="option"
|
||||
/>
|
||||
<div v-else>
|
||||
<div class="p-1.5 px-7 text-base text-gray-500">
|
||||
{{ __('No {0} Available', [field.label]) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
@click="field.create()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NestedPopover>
|
||||
</div>
|
||||
<DateTimePicker
|
||||
v-else-if="field.type === 'Datetime'"
|
||||
v-model="data[field.name]"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<DatePicker
|
||||
v-else-if="field.type === 'Date'"
|
||||
v-model="data[field.name]"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="
|
||||
['Small Text', 'Text', 'Long Text'].includes(field.type)
|
||||
"
|
||||
type="textarea"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
v-model="data[field.name]"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="['Int'].includes(field.type)"
|
||||
type="number"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
v-model="data[field.name]"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
type="text"
|
||||
:placeholder="__(field.placeholder || field.label)"
|
||||
v-model="data[field.name]"
|
||||
:disabled="Boolean(field.read_only)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import DropdownItem from '@/components/DropdownItem.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const props = defineProps({
|
||||
sections: Array,
|
||||
data: Object,
|
||||
})
|
||||
</script>
|
||||
@@ -1,25 +1,270 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Settings'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Save',
|
||||
variant: 'solid',
|
||||
onClick: (close) => {
|
||||
saveSettings(close)
|
||||
},
|
||||
},
|
||||
],
|
||||
}"
|
||||
<Dialog v-model="show" :options="{ size: '6xl' }">
|
||||
<template #body>
|
||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
|
||||
{{ __('Settings') }}
|
||||
</h1>
|
||||
<div v-for="tab in tabs">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<template #body-content> </template>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
<SidebarLink
|
||||
v-for="item in tab.items"
|
||||
:link="item"
|
||||
class="w-full"
|
||||
:class="
|
||||
activeTab?.label == item.label
|
||||
? 'bg-white shadow-sm'
|
||||
: 'hover:bg-gray-100'
|
||||
"
|
||||
@click="activeTab = item"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||
<SettingDetails
|
||||
v-if="activeTab && data.doc"
|
||||
:fields="activeTab.fields"
|
||||
:data="data"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog } from 'frappe-ui'
|
||||
import { Dialog, createDocumentResource } from 'frappe-ui'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import SettingDetails from '../SettingDetails.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const doctype = ref('LMS Settings')
|
||||
const activeTab = ref(null)
|
||||
|
||||
const data = createDocumentResource({
|
||||
doctype: doctype.value,
|
||||
name: doctype.value,
|
||||
fields: ['*'],
|
||||
cache: doctype.value,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
let _tabs = [
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Payment Gateway',
|
||||
icon: 'DollarSign',
|
||||
fields: [
|
||||
{
|
||||
label: 'Razorpay Key',
|
||||
name: 'razorpay_key',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Razorpay Secret',
|
||||
name: 'razorpay_secret',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
label: 'Default Currency',
|
||||
name: 'default_currency',
|
||||
type: 'Link',
|
||||
doctype: 'Currency',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Apply GST for India',
|
||||
name: 'apply_gst',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Show USD equivalent amount',
|
||||
name: 'show_usd_equivalent',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Apply rounding on equivalent amount',
|
||||
name: 'apply_rounding',
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Signup',
|
||||
icon: 'LogIn',
|
||||
fields: [
|
||||
{
|
||||
label: 'Show terms of use on signup page',
|
||||
name: 'terms_of_use',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Terms of Use Page',
|
||||
name: 'terms_page',
|
||||
type: 'Link',
|
||||
doctype: 'Web Page',
|
||||
},
|
||||
{
|
||||
label: 'Ask user category during signup',
|
||||
name: 'user_category',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Show privacy policy on signup page',
|
||||
name: 'privacy_policy',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Privacy Policy Page',
|
||||
name: 'privacy_policy_page',
|
||||
type: 'Link',
|
||||
doctype: 'Web Page',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Show cookie policy on signup page',
|
||||
name: 'cookie_policy',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Cookie Policy Page',
|
||||
name: 'cookie_policy_page',
|
||||
type: 'Link',
|
||||
doctype: 'Web Page',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Sidebar',
|
||||
icon: 'PanelLeftIcon',
|
||||
fields: [
|
||||
{
|
||||
label: 'Courses',
|
||||
name: 'courses',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Batches',
|
||||
name: 'batches',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Certified Participants',
|
||||
name: 'certified_participants',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Jobs',
|
||||
name: 'jobs',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Statistics',
|
||||
name: 'statistics',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
name: 'notifications',
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Email Templates',
|
||||
icon: 'MailPlus',
|
||||
fields: [
|
||||
{
|
||||
label: 'Batch Confirmation Template',
|
||||
name: 'batch_confirmation_template',
|
||||
doctype: 'Email Template',
|
||||
type: 'Link',
|
||||
},
|
||||
{
|
||||
label: 'Certification Template',
|
||||
name: 'certification_template',
|
||||
doctype: 'Email Template',
|
||||
type: 'Link',
|
||||
},
|
||||
{
|
||||
label: 'Assignment Submission Template',
|
||||
name: 'assignment_submission_template',
|
||||
doctype: 'Email Template',
|
||||
type: 'Link',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
/* {
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
icon: "UserRoundPlus",
|
||||
component: markRaw(MemberSettings),
|
||||
},
|
||||
],
|
||||
}, */
|
||||
]
|
||||
|
||||
return _tabs.map((tab) => {
|
||||
tab.items = tab.items.filter((item) => {
|
||||
if (item.condition) {
|
||||
return item.condition()
|
||||
}
|
||||
return true
|
||||
})
|
||||
return tab
|
||||
})
|
||||
})
|
||||
|
||||
watch(show, () => {
|
||||
if (show.value) {
|
||||
activeTab.value = tabs.value[0].items[0]
|
||||
} else {
|
||||
activeTab.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
89
frontend/src/components/SettingDetails.vue
Normal file
89
frontend/src/components/SettingDetails.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-between h-full p-8">
|
||||
<div class="flex space-x-10">
|
||||
<div v-for="(column, index) in columns" :key="index">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div v-for="field in column" :class="width">
|
||||
<Link
|
||||
v-if="field.type == 'Link'"
|
||||
v-model="field.value"
|
||||
:doctype="field.doctype"
|
||||
:label="field.label"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
:key="field.name"
|
||||
v-model="field.value"
|
||||
:label="field.label"
|
||||
:type="field.type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mt-auto">
|
||||
<Button variant="solid" :loading="data.save.loading" @click="update">
|
||||
{{ __('Update') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FormControl, Button } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
let width = ref('w-full')
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
const cols = []
|
||||
let currentColumn = []
|
||||
|
||||
props.fields.forEach((field) => {
|
||||
if (field.type === 'Column Break') {
|
||||
if (currentColumn.length > 0) {
|
||||
cols.push(currentColumn)
|
||||
currentColumn = []
|
||||
}
|
||||
} else {
|
||||
if (field.type == 'checkbox') {
|
||||
field.value = props.data.doc[field.name] ? true : false
|
||||
} else {
|
||||
field.value = props.data.doc[field.name]
|
||||
}
|
||||
currentColumn.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
if (currentColumn.length > 0) {
|
||||
cols.push(currentColumn)
|
||||
}
|
||||
|
||||
if (cols.length == 3) {
|
||||
width.value = 'w-64'
|
||||
} else {
|
||||
width.value = 'w-96'
|
||||
}
|
||||
|
||||
return cols
|
||||
})
|
||||
|
||||
const update = () => {
|
||||
props.fields.forEach((f) => {
|
||||
props.data.doc[f.name] = f.value
|
||||
})
|
||||
props.data.save.submit()
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dropdown :options="userDropdownOptions">
|
||||
<Dropdown class="p-2" :options="userDropdownOptions">
|
||||
<template v-slot="{ open }">
|
||||
<button
|
||||
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
||||
@@ -56,7 +56,10 @@
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<SettingsModal v-model="showSettingsModal" />
|
||||
<SettingsModal
|
||||
v-if="userResource.data?.is_moderator"
|
||||
v-model="showSettingsModal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -121,7 +124,6 @@ const userDropdownOptions = [
|
||||
showSettingsModal.value = true
|
||||
},
|
||||
condition: () => {
|
||||
console.log(userResource)
|
||||
return userResource.data?.is_moderator
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@input="courses.reload()"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="w-4 h-4 stroke-1.5" name="search" />
|
||||
<Search class="w-4 h-4 stroke-1.5 text-gray-600" name="search" />
|
||||
</template>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
@@ -266,7 +266,9 @@ def get_chart_details():
|
||||
"upcoming": 0,
|
||||
},
|
||||
)
|
||||
details.users = frappe.db.count("User", {"enabled": 1})
|
||||
details.users = frappe.db.count(
|
||||
"User", {"enabled": 1, "name": ["not in", ("Administrator", "Guest")]}
|
||||
)
|
||||
details.completions = frappe.db.count(
|
||||
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
||||
)
|
||||
@@ -560,3 +562,29 @@ def get_categories(doctype, filters):
|
||||
categoryOptions.append({"label": category, "value": category})
|
||||
|
||||
return categoryOptions
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
|
||||
not_allowed_fieldtypes = list(frappe.model.no_value_fields) + ["Read Only"]
|
||||
if allow_all_fieldtypes:
|
||||
not_allowed_fieldtypes = []
|
||||
fields = frappe.get_meta(doctype).fields
|
||||
|
||||
_fields = []
|
||||
|
||||
for field in fields:
|
||||
if field.fieldtype not in not_allowed_fieldtypes and field.fieldname:
|
||||
_fields.append(
|
||||
{
|
||||
"label": field.label,
|
||||
"type": field.fieldtype,
|
||||
"value": field.fieldname,
|
||||
"options": field.options,
|
||||
"mandatory": field.reqd,
|
||||
"read_only": field.read_only,
|
||||
"hidden": field.hidden,
|
||||
}
|
||||
)
|
||||
|
||||
return _fields
|
||||
|
||||
@@ -517,13 +517,6 @@ def can_create_courses(course, member=None):
|
||||
if has_course_instructor_role(member) and member in instructors:
|
||||
return True
|
||||
|
||||
portal_course_creation = frappe.db.get_single_value(
|
||||
"LMS Settings", "portal_course_creation"
|
||||
)
|
||||
|
||||
if portal_course_creation == "Anyone" and member in instructors:
|
||||
return True
|
||||
|
||||
if not course and has_course_instructor_role(member):
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{% set search_placeholder = frappe.db.get_single_value("LMS Settings", "search_placeholder") %}
|
||||
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
|
||||
|
||||
|
||||
<div class="modal fade search-modal" id="search-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<input class="search search-course" id="search-course" placeholder="{{ _(search_placeholder) or 'Search for courses' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script> {% include "lms/templates/search_course/search_course.js" %} </script>
|
||||
@@ -1,72 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
$("#search-course").keyup((e) => {
|
||||
search_course(e);
|
||||
});
|
||||
|
||||
$("#open-search").click((e) => {
|
||||
show_search_bar(e);
|
||||
});
|
||||
|
||||
$("#search-modal").on("hidden.bs.modal", () => {
|
||||
hide_search_bar();
|
||||
});
|
||||
|
||||
$(document).keydown(function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key == "k") {
|
||||
show_search_bar(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const search_course = (e) => {
|
||||
let input = $(e.currentTarget).val();
|
||||
if (input == window.input) return;
|
||||
window.input = input;
|
||||
|
||||
if (input.length < 3 || input.trim() == "") {
|
||||
$(".result-row").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.search_course",
|
||||
args: {
|
||||
text: input,
|
||||
},
|
||||
callback: (data) => {
|
||||
render_course_list(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const render_course_list = (data) => {
|
||||
let courses = data.message;
|
||||
$(".result-row").remove();
|
||||
|
||||
if (!courses.length) {
|
||||
let element = `<a class="result-row">
|
||||
${__("No result found")}
|
||||
</a>`;
|
||||
$(element).insertAfter("#search-course");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in courses) {
|
||||
let element = `<a class="result-row" href="/courses/${courses[i].name}">
|
||||
${courses[i].title}
|
||||
</a>`;
|
||||
$(element).insertAfter("#search-course");
|
||||
}
|
||||
};
|
||||
|
||||
const show_search_bar = (e) => {
|
||||
$("#search-modal").modal("show");
|
||||
setTimeout(() => {
|
||||
$("#search-course").focus();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const hide_search_bar = (e) => {
|
||||
$("#search-course").val("");
|
||||
$(".result-row").remove();
|
||||
};
|
||||
Reference in New Issue
Block a user