Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c7915e2
feat: launch command palette
pateljannat Nov 17, 2025
eab43a6
feat: improved search results in command palette
pateljannat Nov 25, 2025
552b584
chore: fixed merge conflicts
pateljannat Dec 8, 2025
d82517f
test: utils
pateljannat Dec 8, 2025
196d4a8
test: utils
pateljannat Dec 8, 2025
c901a15
test: utils
pateljannat Dec 9, 2025
7e3c5be
fix: filter search records based on roles
pateljannat Dec 10, 2025
4c98282
feat: search page
pateljannat Dec 10, 2025
3de5fb0
Merge branch 'develop' of https://github.com/frappe/lms into full-tex…
pateljannat Dec 11, 2025
820ea7e
feat: search page functionality
pateljannat Dec 11, 2025
819318d
feat: broke down sidebar into categories
pateljannat Dec 12, 2025
f49bb98
fix: sidebar improvements
pateljannat Dec 12, 2025
1bc610b
feat: search jobs from command palette
pateljannat Dec 12, 2025
f783c6a
chore: update POT file
frappe-pr-bot Dec 12, 2025
e150225
chore: Hungarian translations
pateljannat Dec 13, 2025
b0a9664
chore: Remove deprecated doctype and related links
rehanrehman389 Dec 14, 2025
810635d
Merge pull request #1906 from frappe/pot_develop_2025-12-12
pateljannat Dec 15, 2025
46b4678
Merge pull request #1908 from frappe/l10n_develop2
pateljannat Dec 15, 2025
41660a1
Merge pull request #1895 from pateljannat/full-text-search
pateljannat Dec 15, 2025
7bf6311
Merge branch 'develop' of https://github.com/frappe/lms into tests-1
pateljannat Dec 15, 2025
8d41a3d
test: certificate and rating test
pateljannat Dec 15, 2025
38e6320
Merge pull request #1890 from pateljannat/tests-1
pateljannat Dec 15, 2025
6cabb4e
chore: resolved conflicts
pateljannat Dec 15, 2025
d3a27e8
fix: enrollment eligibility validation
pateljannat Dec 15, 2025
1210a6a
chore: beautifulsoup dependency management
pateljannat Dec 15, 2025
2596b85
Merge pull request #1913 from rehanrehman389/cleanup-cohort
pateljannat Dec 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ declare module 'vue' {
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
CommandPalette: typeof import('./src/components/CommandPalette/CommandPalette.vue')['default']
CommandPaletteGroup: typeof import('./src/components/CommandPalette/CommandPaletteGroup.vue')['default']
Configuration: typeof import('./src/components/Sidebar/Configuration.vue')['default']
ContactUsEmail: typeof import('./src/components/ContactUsEmail.vue')['default']
CouponDetails: typeof import('./src/components/Settings/Coupons/CouponDetails.vue')['default']
Expand Down
272 changes: 272 additions & 0 deletions frontend/src/components/CommandPalette/CommandPalette.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
<template>
<Dialog v-model="show" :options="{ size: '2xl' }">
<template #body>
<div class="text-base">
<div class="flex items-center space-x-2 pl-4.5 border-b">
<Search class="size-4 text-ink-gray-4" />
<input
ref="inputRef"
type="text"
placeholder="Search"
class="w-full border-none bg-transparent py-3 !pl-2 pr-4.5 text-base text-ink-gray-7 placeholder-ink-gray-4 focus:ring-0"
@input="onInput"
v-model="query"
autocomplete="off"
/>
</div>

<div class="max-h-96 overflow-auto mb-2">
<div v-if="query.length" class="mt-5 space-y-5">
<CommandPaletteGroup
:list="searchResults"
@navigateTo="navigateTo"
/>
</div>

<div v-else class="mt-5 space-y-5">
<CommandPaletteGroup
:list="jumpToOptions"
@navigateTo="navigateTo"
/>
</div>
</div>

<div
class="flex items-center space-x-5 w-full border-t py-2 text-sm text-ink-gray-7 px-4.5"
>
<div class="flex items-center space-x-2">
<MoveUp
class="size-5 stroke-1.5 bg-surface-gray-2 p-1 rounded-sm"
/>
<MoveDown
class="size-5 stroke-1.5 bg-surface-gray-2 p-1 rounded-sm"
/>
<span>
{{ __('to navigate') }}
</span>
</div>
<div class="flex items-center space-x-2">
<CornerDownLeft
class="size-5 stroke-1.5 bg-surface-gray-2 p-1 rounded-sm"
/>
<span>
{{ __('to select') }}
</span>
</div>
<div class="flex items-center space-x-2">
<span class="bg-surface-gray-2 p-1 rounded-sm"> esc </span>
<span>
{{ __('to close') }}
</span>
</div>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { createResource, debounce, Dialog } from 'frappe-ui'
import { nextTick, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
BookOpen,
Briefcase,
CornerDownLeft,
FileSearch,
MoveUp,
MoveDown,
Search,
Users,
} from 'lucide-vue-next'
import CommandPaletteGroup from './CommandPaletteGroup.vue'

const show = defineModel<boolean>({ required: true, default: false })
const router = useRouter()
const query = ref<string>('')
const searchResults = ref<Array<any>>([])

const search = createResource({
url: 'lms.command_palette.search_sqlite',
makeParams: () => ({
query: query.value,
}),
onSuccess() {
generateSearchResults()
},
})

const debouncedSearch = debounce(() => {
if (query.value.length > 2) {
search.reload()
}
}, 500)

const onInput = () => {
debouncedSearch()
}

const generateSearchResults = () => {
search.data?.forEach((type: any) => {
let result: { title: string; items: any[] } = { title: '', items: [] }
result.title = type.title
type.items.forEach((item: any) => {
let paramName = item.doctype === 'LMS Course' ? 'courseName' : 'batchName'
item.route = {
name: item.doctype === 'LMS Course' ? 'CourseDetail' : 'BatchDetail',
params: {
[paramName]: item.name,
},
}
item.isActive = false
})
result.items = type.items
searchResults.value.push(result)
})
}

const appendSearchPage = () => {
let searchPage: { title: string; items: Array<any> } = {
title: '',
items: [],
}
searchPage.title = __('Jump to')
searchPage.items = [
{
title: __('Search for ') + `"${query.value}"`,
route: {
name: 'Search',
query: {
q: query.value,
},
},
icon: FileSearch,
isActive: true,
},
]
searchResults.value = [searchPage]
}

watch(
query,
() => {
appendSearchPage()
},
{ immediate: true }
)

watch(show, () => {
if (!show.value) {
query.value = ''
searchResults.value = []
}
})

onMounted(() => {
addKeyboardShortcuts()
})

const addKeyboardShortcuts = () => {
window.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'ArrowUp' && show.value) {
e.preventDefault()
shortcutForArrowKey(-1)
} else if (e.key === 'ArrowDown' && show.value) {
shortcutForArrowKey(1)
} else if (e.key === 'Enter' && show.value) {
shortcutForEnter()
} else if (e.key === 'Escape' && show.value) {
show.value = false
}
})
}

const shortcutForArrowKey = (direction: number) => {
let currentList = query.value.length
? searchResults.value
: jumpToOptions.value
let allItems = currentList.flatMap((result: any) => result.items)
let indexOfActive = allItems.findIndex((option: any) => option.isActive)
let newIndex = indexOfActive + direction
if (newIndex < 0) newIndex = allItems.length - 1
if (newIndex >= allItems.length) newIndex = 0
allItems[indexOfActive].isActive = false
allItems[newIndex].isActive = true
nextTick(scrollActiveItemIntoView)
}

const scrollActiveItemIntoView = () => {
const activeItem = document.querySelector(
'.hover\\:bg-surface-gray-2.bg-surface-gray-2'
) as HTMLElement
if (activeItem) {
activeItem.scrollIntoView({ block: 'nearest' })
}
}

const shortcutForEnter = () => {
let currentList = query.value.length
? searchResults.value
: jumpToOptions.value
let allItems = currentList.flatMap((result: any) => result.items)
let activeOption = allItems.find((option) => option.isActive)
if (activeOption) {
navigateTo(activeOption.route)
}
}

const navigateTo = (route: {
name: string
params?: Record<string, any>
query?: Record<string, any>
}) => {
show.value = false
query.value = ''
router.replace({ name: route.name, params: route.params, query: route.query })
}

const jumpToOptions = ref([
{
title: __('Jump to'),
items: [
{
title: 'Advanced Search',
icon: Search,
route: {
name: 'Search',
},
isActive: true,
},
{
title: 'Courses',
icon: BookOpen,
route: {
name: 'Courses',
},
isActive: false,
},
{
title: 'Batches',
icon: Users,
route: {
name: 'Batches',
},
isActive: false,
},
{
title: 'Jobs',
icon: Briefcase,
route: {
name: 'Jobs',
},
isActive: false,
},
],
},
])
</script>
<style>
mark {
background-color: theme('colors.amber.100');
font-weight: 500;
}
</style>
45 changes: 45 additions & 0 deletions frontend/src/components/CommandPalette/CommandPaletteGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<div v-for="result in list" class="px-2.5 space-y-2">
<div class="text-ink-gray-5 px-2">
{{ result.title }}
</div>
<div class="">
<div
v-for="item in result.items"
class="flex items-center justify-between p-2 rounded hover:bg-surface-gray-2 cursor-pointer"
:class="{ 'bg-surface-gray-2': item.isActive }"
@click="emit('navigateTo', item.route)"
>
<div class="flex items-center space-x-3">
<component
v-if="item.icon"
:is="item.icon"
class="size-4 stroke-1.5 text-ink-gray-6"
/>
<div v-html="item.title"></div>
</div>
<div v-if="item.modified" class="text-ink-gray-5">
{{ dayjs.unix(item.modified).fromNow(true) }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { inject } from 'vue'

const dayjs = inject<any>('$dayjs')
const emit = defineEmits(['navigateTo'])

const props = defineProps<{
list: Array<{
title: string
items: Array<{
title: string
icon?: any
isActive?: boolean
modified?: string
}>
}>
}>()
</script>
12 changes: 1 addition & 11 deletions frontend/src/components/CourseCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,7 @@ const getGradientColor = () => {
localStorage.getItem('theme') == 'light' ? 'lightMode' : 'darkMode'
let color = props.course.card_gradient?.toLowerCase() || 'blue'
let colorMap = colors[theme][color]
return `linear-gradient(to top right, black, ${colorMap[400]})`
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
/* return `radial-gradient(ellipse at 80% 20%, black 20%, ${colorMap[500]} 100%)` */
/* return `radial-gradient(ellipse at 30% 70%, black 50%, ${colorMap[500]} 100%)` */
/* return `radial-gradient(ellipse at 80% 20%, ${colorMap[100]} 0%, ${colorMap[300]} 50%, ${colorMap[500]} 100%)` */
/* return `conic-gradient(from 180deg at 50% 50%, ${colorMap[100]} 0%, ${colorMap[200]} 50%, ${colorMap[400]} 100%)` */
/* return `linear-gradient(135deg, ${colorMap[100]}, ${colorMap[300]}), linear-gradient(120deg, rgba(255,255,255,0.4) 0%, transparent 60%) ` */
/* return `radial-gradient(circle at 20% 30%, ${colorMap[100]} 0%, transparent 40%),
radial-gradient(circle at 80% 40%, ${colorMap[200]} 0%, transparent 50%),
linear-gradient(135deg, ${colorMap[300]} 0%, ${colorMap[400]} 100%);` */
return `linear-gradient(to top right, black, ${colorMap[200]})`
}
</script>
<style>
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/CourseOutline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
name: allowEdit ? 'LessonForm' : 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
lessonNumber: lesson.number.split('.')[1],
chapterNumber: lesson.number.split('-')[0],
lessonNumber: lesson.number.split('-')[1],
},
}"
>
Expand Down Expand Up @@ -389,8 +389,8 @@ const redirectToChapter = (chapter) => {

const isActiveLesson = (lessonNumber) => {
return (
route.params.chapterNumber == lessonNumber.split('.')[0] &&
route.params.lessonNumber == lessonNumber.split('.')[1]
route.params.chapterNumber == lessonNumber.split('-')[0] &&
route.params.lessonNumber == lessonNumber.split('-')[1]
)
}
</script>
7 changes: 5 additions & 2 deletions frontend/src/components/Modals/JobApplicationModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}"
>
<template #body-content>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-4 text-base">
<p class="text-ink-gray-9">
{{
__(
Expand All @@ -39,6 +39,9 @@
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="">
<Button @click="openFileSelector" :loading="uploading">
<template #prefix>
<Upload class="size-4 stroke-1.5" />
</template>
{{
uploading ? `Uploading ${progress}%` : 'Upload your resume'
}}
Expand Down Expand Up @@ -66,7 +69,7 @@
</template>
<script setup>
import { Dialog, FileUploader, Button, createResource, toast } from 'frappe-ui'
import { FileText } from 'lucide-vue-next'
import { FileText, Upload } from 'lucide-vue-next'
import { ref, inject } from 'vue'
import { getFileSize } from '@/utils/'

Expand Down
Loading
Loading