From eab43a66cfdef3b0e8a631ac3c52931f585efef8 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 25 Nov 2025 19:44:31 +0530 Subject: [PATCH] feat: improved search results in command palette --- frappe-ui | 2 +- frontend/components.d.ts | 4 + .../CommandPalette/CommandPalette.vue | 249 +++++++++++++++++- .../CommandPalette/CommandPaletteGroup.vue | 44 ++++ frontend/src/pages/Search/Search.vue | 11 + frontend/src/router.js | 5 + lms/command_palette.py | 29 ++ lms/hooks.py | 8 + lms/lms/doctype/lms_course/lms_course.json | 6 +- lms/sqlite.py | 122 +++++++++ pyproject.toml | 2 +- 11 files changed, 474 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/CommandPalette/CommandPaletteGroup.vue create mode 100644 frontend/src/pages/Search/Search.vue create mode 100644 lms/command_palette.py create mode 100644 lms/sqlite.py diff --git a/frappe-ui b/frappe-ui index 8d5956c0..204333c9 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit 8d5956c0c675a13d32d3ef8035389128fa4735d5 +Subproject commit 204333c9256f21fca5f5c50acd66cee11aeca4f3 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index fd58e52a..b52f8116 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -43,6 +43,7 @@ declare module 'vue' { 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'] ContactUsEmail: typeof import('./src/components/ContactUsEmail.vue')['default'] CouponDetails: typeof import('./src/components/Settings/Coupons/CouponDetails.vue')['default'] CouponItems: typeof import('./src/components/Settings/Coupons/CouponItems.vue')['default'] @@ -85,6 +86,9 @@ declare module 'vue' { LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default'] LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default'] LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default'] + LucideArrowDown: typeof import('~icons/lucide/arrow-down')['default'] + LucideArrowUp: typeof import('~icons/lucide/arrow-up')['default'] + LucideCornerDownLeft: typeof import('~icons/lucide/corner-down-left')['default'] Members: typeof import('./src/components/Settings/Members.vue')['default'] MobileLayout: typeof import('./src/components/MobileLayout.vue')['default'] MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default'] diff --git a/frontend/src/components/CommandPalette/CommandPalette.vue b/frontend/src/components/CommandPalette/CommandPalette.vue index 25bc3d3e..c4fdf036 100644 --- a/frontend/src/components/CommandPalette/CommandPalette.vue +++ b/frontend/src/components/CommandPalette/CommandPalette.vue @@ -1,10 +1,253 @@ + diff --git a/frontend/src/components/CommandPalette/CommandPaletteGroup.vue b/frontend/src/components/CommandPalette/CommandPaletteGroup.vue new file mode 100644 index 00000000..cf34b593 --- /dev/null +++ b/frontend/src/components/CommandPalette/CommandPaletteGroup.vue @@ -0,0 +1,44 @@ + + diff --git a/frontend/src/pages/Search/Search.vue b/frontend/src/pages/Search/Search.vue new file mode 100644 index 00000000..bc577e7b --- /dev/null +++ b/frontend/src/pages/Search/Search.vue @@ -0,0 +1,11 @@ + + diff --git a/frontend/src/router.js b/frontend/src/router.js index 9b0234f8..c9fba46e 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -243,6 +243,11 @@ const routes = [ ), props: true, }, + { + path: '/search', + name: 'Search', + component: () => import('@/pages/Search/Search.vue'), + }, ] let router = createRouter({ diff --git a/lms/command_palette.py b/lms/command_palette.py new file mode 100644 index 00000000..077c9697 --- /dev/null +++ b/lms/command_palette.py @@ -0,0 +1,29 @@ +import frappe + + +@frappe.whitelist() +def search_sqlite(query: str): + from lms.sqlite import LearningSearch, LearningSearchIndexMissingError + + search = LearningSearch() + + try: + result = search.search(query) + except LearningSearchIndexMissingError: + return [] + + groups = {} + print(result) + for r in result["results"]: + doctype = r["doctype"] + + if doctype == "LMS Course": + groups.setdefault("Courses", []).append(r) + elif doctype == "LMS Batch": + groups.setdefault("Batches", []).append(r) + + out = [] + for key in groups: + out.append({"title": key, "items": groups[key]}) + + return out diff --git a/lms/hooks.py b/lms/hooks.py index 038eee9d..361aa3b2 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -64,6 +64,9 @@ after_install = "lms.install.after_install" after_sync = "lms.install.after_sync" before_uninstall = "lms.install.before_uninstall" setup_wizard_requires = "assets/lms/js/setup_wizard.js" +after_migrate = [ + "lms.sqlite.build_index_in_background", +] # Desk Notifications # ------------------ @@ -112,6 +115,9 @@ doc_events = { # Scheduled Tasks # --------------- scheduler_events = { + "all": [ + "lms.sqlite.build_index_in_background", + ], "hourly": [ "lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals", "lms.lms.api.update_course_statistics", @@ -251,3 +257,5 @@ add_to_apps_screen = [ "has_permission": "lms.lms.api.check_app_permission", } ] + +sqlite_search = ["lms.sqlite.LearningSearch"] diff --git a/lms/lms/doctype/lms_course/lms_course.json b/lms/lms/doctype/lms_course/lms_course.json index 20fc0482..b30ed240 100644 --- a/lms/lms/doctype/lms_course/lms_course.json +++ b/lms/lms/doctype/lms_course/lms_course.json @@ -76,6 +76,8 @@ "default": "0", "fieldname": "published", "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Published" }, { @@ -152,8 +154,6 @@ "fieldname": "status", "fieldtype": "Select", "hidden": 1, - "in_list_view": 1, - "in_standard_filter": 1, "label": "Status", "options": "In Progress\nUnder Review\nApproved", "read_only": 1 @@ -313,7 +313,7 @@ } ], "make_attachments_public": 1, - "modified": "2025-10-13 15:08:11.734204", + "modified": "2025-11-25 11:35:17.924569", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Course", diff --git a/lms/sqlite.py b/lms/sqlite.py new file mode 100644 index 00000000..f8f30e03 --- /dev/null +++ b/lms/sqlite.py @@ -0,0 +1,122 @@ +from contextlib import suppress + +import frappe +from frappe.search.sqlite_search import SQLiteSearch, SQLiteSearchIndexMissingError +from frappe.utils import update_progress_bar +from redis.exceptions import ResponseError + + +class LearningSearch(SQLiteSearch): + INDEX_NAME = "learning.db" + + INDEX_SCHEMA = { + "metadata_fields": ["category", "owner", "published"], + "tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'", + } + + INDEXABLE_DOCTYPES = { + "LMS Course": { + "fields": [ + "name", + "title", + {"content": "description"}, + "short_introduction", + "published", + "category", + "owner", + {"modified": "published_on"}, + ], + }, + "LMS Batch": { + "fields": [ + "name", + "title", + "description", + {"content": "batch_details"}, + "published", + "category", + "owner", + {"modified": "start_date"}, + ], + }, + } + + DOCTYPE_FIELDS = { + "LMS Course": [ + "name", + "title", + "description", + "short_introduction", + "category", + "creation", + "modified", + "owner", + ], + "LMS Batch": [ + "name", + "title", + "description", + "batch_details", + "category", + "creation", + "modified", + "owner", + ], + } + + def can_create_course(self, roles): + return "Course Creator" in roles or "Moderator" in roles + + def can_create_batch(self, roles): + return "Batch Evaluator" in roles or "Moderator" in roles + + def get_records(self, doctype): + records = [] + roles = frappe.get_roles() + filters = {} + + if doctype == "LMS Course": + if not self.can_create_course(roles): + filters = {"published": 1} + + if doctype == "LMS Batch": + if not self.can_create_batch(roles): + filters = {"published": 1} + + records = frappe.db.get_all(doctype, filters=filters, fields=self.DOCTYPE_FIELDS[doctype]) + for record in records: + record["doctype"] = doctype + + return records + + def build_index(self): + try: + super().build_index() + except Exception as e: + frappe.throw(e) + + def get_search_filters(self): + roles = frappe.get_roles() + if not (self.can_create_course(roles) and self.can_create_batch(roles)): + return {"published": 1} + return {} + + +class LearningSearchIndexMissingError(SQLiteSearchIndexMissingError): + pass + + +def build_index(): + search = LearningSearch() + search.build_index() + + +def build_index_in_background(): + if not frappe.cache().get_value("learning_search_indexing_in_progress"): + frappe.enqueue(build_index, queue="long") + + +def build_index_if_not_exists(): + search = LearningSearch() + if not search.index_exists(): + build_index() diff --git a/pyproject.toml b/pyproject.toml index fc16f51d..7d7f4193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ # core dependencies "websocket_client~=1.6.4", "markdown~=3.5.1", - "beautifulsoup4~=4.12.2", + "beautifulsoup4~=4.13.4", "lxml~=4.9.3", "cairocffi==1.5.1", "razorpay~=1.4.1",