feat: category settings

This commit is contained in:
Jannat Patel
2024-09-23 16:11:17 +05:30
parent 7c90ca4040
commit dc2bf9f13e
9 changed files with 218 additions and 49 deletions

View File

@@ -30,9 +30,11 @@
<div class="overflow-y-scroll"> <div class="overflow-y-scroll">
<div class="text-base divide-y"> <div class="text-base divide-y">
<FormControl <FormControl
:value="cat.name" :value="cat.category"
type="text"
v-for="cat in categories.data" v-for="cat in categories.data"
class="py-3" class="form-control"
@change.stop="(e) => update(cat.name, e.target.value)"
/> />
</div> </div>
</div> </div>
@@ -44,6 +46,7 @@ import {
FormControl, FormControl,
createListResource, createListResource,
createResource, createResource,
debounce,
} from 'frappe-ui' } from 'frappe-ui'
import { Plus, X } from 'lucide-vue-next' import { Plus, X } from 'lucide-vue-next'
import { ref } from 'vue' import { ref } from 'vue'
@@ -99,4 +102,52 @@ const showCategoryForm = () => {
categoryInput.value.$el.querySelector('input').focus() categoryInput.value.$el.querySelector('input').focus()
}, 0) }, 0)
} }
const updateCategory = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'LMS Category',
name: values.name,
fieldname: {
category: values.value,
},
}
},
})
const update = debounce((name, value) => {
updateCategory.submit(
{
name: name,
value: value,
},
{
onSuccess() {
categories.reload()
},
}
)
}, 500)
</script> </script>
<style>
.form-control input {
padding: 1.25rem 0;
border-color: transparent;
background: white;
}
.form-control input:focus {
outline: transparent;
background: white;
box-shadow: none;
border-color: transparent;
}
.form-control input:hover {
outline: transparent;
background: white;
box-shadow: none;
border-color: transparent;
}
</style>

View File

@@ -6,7 +6,7 @@
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold"> <h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
{{ __('Settings') }} {{ __('Settings') }}
</h1> </h1>
<div v-for="tab in tabs"> <div v-for="tab in tabs" :key="tab.label">
<div <div
v-if="!tab.hideLabel" 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" 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"
@@ -17,6 +17,7 @@
<SidebarLink <SidebarLink
v-for="item in tab.items" v-for="item in tab.items"
:link="item" :link="item"
:key="item.label"
class="w-full" class="w-full"
:class=" :class="
activeTab?.label == item.label activeTab?.label == item.label
@@ -30,6 +31,7 @@
</div> </div>
<div <div
v-if="activeTab && data.doc" v-if="activeTab && data.doc"
:key="activeTab.label"
class="flex flex-1 flex-col px-10 py-8" class="flex flex-1 flex-col px-10 py-8"
> >
<Members <Members
@@ -58,14 +60,16 @@
<script setup> <script setup>
import { Dialog, createDocumentResource } from 'frappe-ui' import { Dialog, createDocumentResource } from 'frappe-ui'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useSettings } from '@/stores/settings'
import SettingDetails from '../SettingDetails.vue' import SettingDetails from '../SettingDetails.vue'
import SidebarLink from '@/components/SidebarLink.vue' import SidebarLink from '@/components/SidebarLink.vue'
import Members from '@/components/Members.vue' import Members from '@/components/Members.vue'
import Categories from '../Categories.vue' import Categories from '@/components/Categories.vue'
const show = defineModel() const show = defineModel()
const doctype = ref('LMS Settings') const doctype = ref('LMS Settings')
const activeTab = ref(null) const activeTab = ref(null)
const settingsStore = useSettings()
const data = createDocumentResource({ const data = createDocumentResource({
doctype: doctype.value, doctype: doctype.value,
@@ -75,8 +79,8 @@ const data = createDocumentResource({
auto: true, auto: true,
}) })
const tabs = computed(() => { const tabsStructure = computed(() => {
let _tabs = [ return [
{ {
label: 'Settings', label: 'Settings',
hideLabel: true, hideLabel: true,
@@ -86,11 +90,23 @@ const tabs = computed(() => {
description: 'Manage the members of your learning system', description: 'Manage the members of your learning system',
icon: 'UserRoundPlus', icon: 'UserRoundPlus',
}, },
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{ {
label: 'Categories', label: 'Categories',
description: 'Manage the members of your learning system', description: 'Manage the members of your learning system',
icon: 'Network', icon: 'Network',
}, },
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{ {
label: 'Payment Gateway', label: 'Payment Gateway',
icon: 'DollarSign', icon: 'DollarSign',
@@ -136,8 +152,8 @@ const tabs = computed(() => {
], ],
}, },
{ {
label: 'Settings', label: 'Customise',
hideLabel: true, hideLabel: false,
items: [ items: [
{ {
label: 'Sidebar', label: 'Sidebar',
@@ -179,12 +195,6 @@ const tabs = computed(() => {
}, },
], ],
}, },
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{ {
label: 'Email Templates', label: 'Email Templates',
icon: 'MailPlus', icon: 'MailPlus',
@@ -210,12 +220,6 @@ const tabs = computed(() => {
}, },
], ],
}, },
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{ {
label: 'Signup', label: 'Signup',
icon: 'LogIn', icon: 'LogIn',
@@ -268,23 +272,36 @@ const tabs = computed(() => {
], ],
}, },
] ]
})
return _tabs.map((tab) => { const tabs = computed(() => {
tab.items = tab.items.filter((item) => { return tabsStructure.value.map((tab) => {
if (item.condition) { return {
return item.condition() ...tab,
} items: tab.items.filter((item) => {
return true return !item.condition || item.condition()
}) }),
return tab }
}) })
}) })
watch(show, () => { watch(
() => activeTab.value,
(value) => {
console.log('Tab watcher', value)
}
)
watch(show, async () => {
if (show.value) { if (show.value) {
activeTab.value = tabs.value[0].items[0] const currentTab = await tabs.value
.flatMap((tab) => tab.items)
.find((item) => item.label === settingsStore.activeTab)
activeTab.value = currentTab || tabs.value[0].items[0]
} else { } else {
console.log('else')
activeTab.value = null activeTab.value = null
settingsStore.isSettingsOpen = false
} }
}) })
</script> </script>

View File

@@ -67,25 +67,20 @@ import LMSLogo from '@/components/Icons/LMSLogo.vue'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { Dropdown } from 'frappe-ui' import { Dropdown } from 'frappe-ui'
import Apps from '@/components/Apps.vue' import Apps from '@/components/Apps.vue'
import { import { ChevronDown, LogIn, LogOut, User, Settings } from 'lucide-vue-next'
ChevronDown,
LogIn,
LogOut,
User,
ArrowRightLeft,
Settings,
} from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { convertToTitleCase } from '../utils' import { convertToTitleCase } from '../utils'
import { usersStore } from '@/stores/user' import { usersStore } from '@/stores/user'
import { ref, markRaw } from 'vue' import { useSettings } from '@/stores/settings'
import { markRaw, watch, ref } from 'vue'
import SettingsModal from '@/components/Modals/Settings.vue' import SettingsModal from '@/components/Modals/Settings.vue'
const router = useRouter() const router = useRouter()
const showSettingsModal = ref(false)
const { logout, branding } = sessionStore() const { logout, branding } = sessionStore()
let { userResource } = usersStore() let { userResource } = usersStore()
const settingsStore = useSettings()
let { isLoggedIn } = sessionStore() let { isLoggedIn } = sessionStore()
const showSettingsModal = ref(false)
const props = defineProps({ const props = defineProps({
isCollapsed: { isCollapsed: {
@@ -94,6 +89,13 @@ const props = defineProps({
}, },
}) })
watch(
() => settingsStore.isSettingsOpen,
(value) => {
showSettingsModal.value = value
}
)
const userDropdownOptions = [ const userDropdownOptions = [
{ {
icon: User, icon: User,
@@ -118,7 +120,7 @@ const userDropdownOptions = [
icon: Settings, icon: Settings,
label: 'Settings', label: 'Settings',
onClick: () => { onClick: () => {
showSettingsModal.value = true settingsStore.isSettingsOpen = true
}, },
condition: () => { condition: () => {
return userResource.data?.is_moderator return userResource.data?.is_moderator

View File

@@ -109,6 +109,14 @@
/> />
</div> </div>
</div> </div>
<div class="w-1/2 mb-4">
<Link
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:onCreate="(value, close) => openSettings(close)"
/>
</div>
<MultiSelect <MultiSelect
v-model="instructors" v-model="instructors"
doctype="User" doctype="User"
@@ -221,18 +229,20 @@ import {
showToast, showToast,
getFileSize, getFileSize,
updateDocumentTitle, updateDocumentTitle,
} from '../utils' } from '@/utils'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useSettings } from '@/stores/settings'
const user = inject('$user') const user = inject('$user')
const newTag = ref('') const newTag = ref('')
const router = useRouter() const router = useRouter()
const instructors = ref([]) const instructors = ref([])
const settingsStore = useSettings()
const props = defineProps({ const props = defineProps({
courseName: { courseName: {
@@ -463,6 +473,12 @@ const removeImage = () => {
course.course_image = null course.course_image = null
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Categories'
settingsStore.isSettingsOpen = true
}
const check_permission = () => { const check_permission = () => {
let user_is_instructor = false let user_is_instructor = false
if (user.data?.is_moderator) return if (user.data?.is_moderator) return

View File

@@ -8,6 +8,15 @@
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]" :items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
/> />
<div class="flex space-x-2 justify-end"> <div class="flex space-x-2 justify-end">
<div class="w-44">
<FormControl
v-if="categories.data?.length"
type="select"
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
/>
</div>
<div class="w-36"> <div class="w-36">
<FormControl <FormControl
type="text" type="text"
@@ -119,11 +128,19 @@ import {
} from 'frappe-ui' } from 'frappe-ui'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import { Plus, Search } from 'lucide-vue-next' import { Plus, Search } from 'lucide-vue-next'
import { ref, computed, inject } from 'vue' import { ref, computed, inject, onMounted, watch } from 'vue'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
const user = inject('$user') const user = inject('$user')
const searchQuery = ref('') const searchQuery = ref('')
const currentCategory = ref(null)
onMounted(() => {
let queries = new URLSearchParams(location.search)
if (queries.has('category')) {
currentCategory.value = queries.get('category')
}
})
const courses = createResource({ const courses = createResource({
url: 'lms.lms.utils.get_courses', url: 'lms.lms.utils.get_courses',
@@ -168,18 +185,57 @@ const addToTabs = (label) => {
} }
const getCourses = (type) => { const getCourses = (type) => {
let courseList = courses.data[type]
if (searchQuery.value) { if (searchQuery.value) {
let query = searchQuery.value.toLowerCase() let query = searchQuery.value.toLowerCase()
return courses.data[type].filter( courseList = courseList.filter(
(course) => (course) =>
course.title.toLowerCase().includes(query) || course.title.toLowerCase().includes(query) ||
course.short_introduction.toLowerCase().includes(query) || course.short_introduction.toLowerCase().includes(query) ||
course.tags.filter((tag) => tag.toLowerCase().includes(query)).length course.tags.filter((tag) => tag.toLowerCase().includes(query)).length
) )
} }
return courses.data[type] if (currentCategory.value && currentCategory.value != '') {
courseList = courseList.filter(
(course) => course.category == currentCategory.value
)
}
return courseList
} }
const categories = createResource({
url: 'lms.lms.api.get_categories',
makeParams() {
return {
doctype: 'LMS Course',
filters: {
published: 1,
},
}
},
cache: ['courseCategories'],
auto: true,
transform(data) {
data.unshift({
label: '',
value: null,
})
},
})
watch(
() => currentCategory.value,
() => {
let queries = new URLSearchParams(location.search)
if (currentCategory.value) {
queries.set('category', currentCategory.value)
} else {
queries.delete('category')
}
history.pushState(null, '', `${location.pathname}?${queries.toString()}`)
}
)
const pageMeta = computed(() => { const pageMeta = computed(() => {
return { return {
title: 'Courses', title: 'Courses',

View File

@@ -0,0 +1,12 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettings = defineStore('settings', () => {
const isSettingsOpen = ref(false)
const activeTab = ref(null)
return {
isSettingsOpen,
activeTab,
}
})

View File

@@ -15,12 +15,13 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Category", "label": "Category",
"reqd": 1,
"unique": 1 "unique": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-06-15 15:14:11.341961", "modified": "2024-09-23 15:25:21.319427",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Category", "name": "LMS Category",
@@ -55,5 +56,6 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "category" "title_field": "category",
"track_changes": 1
} }

View File

@@ -16,10 +16,12 @@
"field_order": [ "field_order": [
"title", "title",
"video_link", "video_link",
"image",
"column_break_3", "column_break_3",
"instructors", "instructors",
"tags", "tags",
"column_break_htgn",
"image",
"category",
"status", "status",
"section_break_7", "section_break_7",
"published", "published",
@@ -237,6 +239,16 @@
"fieldname": "certification_tab", "fieldname": "certification_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Certification" "label": "Certification"
},
{
"fieldname": "column_break_htgn",
"fieldtype": "Column Break"
},
{
"fieldname": "category",
"fieldtype": "Link",
"label": "Category",
"options": "LMS Category"
} }
], ],
"is_published_field": "published", "is_published_field": "published",
@@ -263,7 +275,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2024-07-12 13:54:40.474097", "modified": "2024-09-21 10:23:58.633912",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -1220,6 +1220,7 @@ def get_course_details(course):
"featured", "featured",
"disable_self_learning", "disable_self_learning",
"published_on", "published_on",
"category",
"status", "status",
"paid_course", "paid_course",
"course_price", "course_price",