feat: settings
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,4 +11,5 @@ __pycache__/
|
|||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
lms/public/frontend
|
lms/public/frontend
|
||||||
lms/www/lms.html
|
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 install-app lms
|
||||||
bench --site lms.localhost set-config developer_mode 1
|
bench --site lms.localhost set-config developer_mode 1
|
||||||
bench --site lms.localhost clear-cache
|
bench --site lms.localhost clear-cache
|
||||||
bench --site lms.localhost set-config mute_emails 1
|
|
||||||
bench use lms.localhost
|
bench use lms.localhost
|
||||||
|
|
||||||
bench start
|
bench start
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="flex flex-col overflow-hidden"
|
class="flex flex-col overflow-hidden"
|
||||||
:class="isSidebarCollapsed ? 'items-center' : ''"
|
:class="isSidebarCollapsed ? 'items-center' : ''"
|
||||||
>
|
>
|
||||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
||||||
<div class="flex flex-col" v-if="sidebarSettings.data">
|
<div class="flex flex-col" v-if="sidebarSettings.data">
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
v-for="link in sidebarLinks"
|
v-for="link in sidebarLinks"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Star } from 'lucide-vue-next'
|
import { Star } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
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>
|
<template>
|
||||||
<Dialog
|
<Dialog v-model="show" :options="{ size: '6xl' }">
|
||||||
v-model="show"
|
<template #body>
|
||||||
:options="{
|
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||||
title: __('Settings'),
|
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
||||||
size: 'xl',
|
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
|
||||||
actions: [
|
{{ __('Settings') }}
|
||||||
{
|
</h1>
|
||||||
label: 'Save',
|
<div v-for="tab in tabs">
|
||||||
variant: 'solid',
|
<div
|
||||||
onClick: (close) => {
|
v-if="!tab.hideLabel"
|
||||||
saveSettings(close)
|
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"
|
||||||
},
|
>
|
||||||
},
|
<span>{{ __(tab.label) }}</span>
|
||||||
],
|
</div>
|
||||||
}"
|
<nav class="space-y-1">
|
||||||
>
|
<SidebarLink
|
||||||
<template #body-content> </template>
|
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>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<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 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>
|
</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>
|
<template>
|
||||||
<Dropdown :options="userDropdownOptions">
|
<Dropdown class="p-2" :options="userDropdownOptions">
|
||||||
<template v-slot="{ open }">
|
<template v-slot="{ open }">
|
||||||
<button
|
<button
|
||||||
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
||||||
@@ -56,7 +56,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<SettingsModal v-model="showSettingsModal" />
|
<SettingsModal
|
||||||
|
v-if="userResource.data?.is_moderator"
|
||||||
|
v-model="showSettingsModal"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -121,7 +124,6 @@ const userDropdownOptions = [
|
|||||||
showSettingsModal.value = true
|
showSettingsModal.value = true
|
||||||
},
|
},
|
||||||
condition: () => {
|
condition: () => {
|
||||||
console.log(userResource)
|
|
||||||
return userResource.data?.is_moderator
|
return userResource.data?.is_moderator
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
@input="courses.reload()"
|
@input="courses.reload()"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<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>
|
</template>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -266,7 +266,9 @@ def get_chart_details():
|
|||||||
"upcoming": 0,
|
"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(
|
details.completions = frappe.db.count(
|
||||||
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
||||||
)
|
)
|
||||||
@@ -560,3 +562,29 @@ def get_categories(doctype, filters):
|
|||||||
categoryOptions.append({"label": category, "value": category})
|
categoryOptions.append({"label": category, "value": category})
|
||||||
|
|
||||||
return categoryOptions
|
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:
|
if has_course_instructor_role(member) and member in instructors:
|
||||||
return True
|
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):
|
if not course and has_course_instructor_role(member):
|
||||||
return True
|
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