diff --git a/drive/api/files.py b/drive/api/files.py index 514e2313e..1e541af7f 100644 --- a/drive/api/files.py +++ b/drive/api/files.py @@ -150,44 +150,47 @@ def upload_file( return drive_file +IMAGE_THUMBNAILS = ["Image", "Video", "PDF", "Presentation"] + + +def _get_default_thumbnail(file_type: str) -> BytesIO: + file_path = frappe.get_app_path("drive", "public", "images", "icons", f"{file_type.lower()}.svg") + try: + with open(file_path, "rb") as f: + return BytesIO(f.read()) + except FileNotFoundError as e: + return None + + @frappe.whitelist(allow_guest=True) -def get_thumbnail(entity_name): - drive_file = frappe.get_value( +def get_thumbnail(entity_name, image=True): + drive_file = frappe.get_cached_doc( "Drive File", entity_name, - [ - "is_group", - "path", - "title", - "mime_type", - "file_size", - "owner", - "team", - "document", - "name", - ], - as_dict=1, - ) - if not drive_file or drive_file.is_group or drive_file.is_link: + ).as_dict() + if not drive_file or not user_has_permission(drive_file, "read"): return - if user_has_permission(drive_file, "read") is False: - return - thumbnail_data = None + default = False + if frappe.cache().exists(entity_name): try: - thumbnail_data = frappe.cache().get_value(entity_name) + cache = frappe.cache().get_value(entity_name) + thumbnail_data = cache["thumbnail"] + default = cache["default"] except: frappe.cache().delete_value(entity_name) + if not thumbnail_data: + file_type = get_file_type(dict(drive_file)) manager = FileManager() try: - if drive_file.mime_type.startswith("text"): + if not image and drive_file.mime_type.startswith("text"): with manager.get_file(drive_file) as f: thumbnail_data = f.read()[:1000].decode("utf-8").replace("\n", "
") - elif drive_file.mime_type == "frappe_doc": + elif not image and drive_file.mime_type == "frappe_doc": html = frappe.get_value("Drive Document", drive_file.document, "raw_content") - thumbnail_data = html[:1000] if html else "" + thumbnail_data = html[:1000] elif drive_file.mime_type == "frappe/slides": # Use this until the thumbnail method is whitelisted thumbnails = frappe.call( @@ -197,22 +200,32 @@ def get_thumbnail(entity_name): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = thumbnails[0] return - else: + elif file_type in IMAGE_THUMBNAILS: thumbnail = manager.get_thumbnail(drive_file.team, entity_name) thumbnail_data = BytesIO(thumbnail.read()) thumbnail.close() except: - return "" - - if thumbnail_data: - frappe.cache().set_value(entity_name, thumbnail_data, expires_in_sec=60 * 60) + pass + finally: + if not thumbnail_data: + thumbnail_data = _get_default_thumbnail(file_type) + default = True + if thumbnail_data: + frappe.cache().set_value( + entity_name, {"thumbnail": thumbnail_data, "default": default}, expires_in_sec=60 * 60 + ) if isinstance(thumbnail_data, BytesIO): response = Response( wrap_file(frappe.request.environ, thumbnail_data), direct_passthrough=True, ) - response.headers.set("Content-Type", "image/jpeg") + if default: + response.headers.set("Content-Type", "image/svg+xml") + response.headers.set("Cache-Control", "public, max-age=3600") + else: + response.headers.set("Content-Type", "image/jpeg") + response.headers.set("X-Thumbnail-Default", "1" if default else "0") response.headers.set("Content-Disposition", "inline", filename=entity_name) return response else: diff --git a/drive/api/list.py b/drive/api/list.py index 27fb4726d..d29daf3e2 100644 --- a/drive/api/list.py +++ b/drive/api/list.py @@ -27,10 +27,11 @@ def files( team, entity_name=None, + parent=None, order_by="modified 1", is_active=1, - limit=20, - cursor=None, + limit=1000, + start=0, favourites_only=0, recents_only=0, shared=None, @@ -44,9 +45,13 @@ def files( field, ascending = order_by.replace("modified", "_modified").split(" ") is_active = int(is_active) only_parent = int(only_parent) + limit = int(limit) + start = int(start) folders = int(folders) favourites_only = int(favourites_only) ascending = int(ascending) + if not entity_name: + entity_name = parent all_teams = False if team == "all": @@ -99,10 +104,6 @@ def files( fn.Coalesce(DrivePermission.read, 1).as_("read") == 1 ) - # Cursor pagination - if cursor: - query = query.where((Binary(DriveFile[field]) > cursor if ascending else field < cursor)).limit(limit) - # Cleaner way? if only_parent and (not recents_only and not favourites_only and not shared): query = query.where(DriveFile.parent_entity == entity_name) @@ -156,8 +157,15 @@ def files( if folders: query = query.where(DriveFile.is_group == 1) + query = query.limit(limit + 1).offset(start) res = query.run(as_dict=True) + # Check if there's a next page + has_next_page = len(res) > limit + + # Remove the extra record if it exists + if has_next_page: + res = res[:limit] child_count_query = ( frappe.qb.from_(DriveFile) .where((DriveFile.team == team) & (DriveFile.is_active == 1)) @@ -208,6 +216,11 @@ def files( else: r["share_count"] = default r |= get_user_access(r["name"]) + + # Return in the format useList expects + frappe.response["data"] = res + frappe.response["has_next_page"] = has_next_page + return res @@ -217,3 +230,8 @@ def get_transfers(): "Drive Transfer", filters={"owner": frappe.session.user}, fields=["title", "file_size", "creation", "name"] ) return transfers + + +@frappe.whitelist(allow_guest=True) +def search(query, parent=None, limit=None): + return files(parent=parent, limit=limit, search=query) diff --git a/drive/api/notifications.py b/drive/api/notifications.py index 3b9081243..bee184a52 100644 --- a/drive/api/notifications.py +++ b/drive/api/notifications.py @@ -8,14 +8,19 @@ def get_link(entity): @frappe.whitelist() -def get_notifications(only_unread): +def get_notifications(only_unread, limit=20, offset=0): """ - Get notifications for current user - - :param only_unread: only get notifications where read is False + Get notifications for current user (paged) + :param limit: page size + :param offset: offset for pagination """ + + limit = int(limit) + offset = int(offset) + User = frappe.qb.DocType("User") Notification = frappe.qb.DocType("Drive Notification") + fields = [ Notification.name, Notification.to_user, @@ -30,19 +35,28 @@ def get_notifications(only_unread): User.user_image, User.full_name, ] + query = ( frappe.qb.from_(Notification) .left_join(User) .on(Notification.from_user == User.name) .select(*fields) + .where(Notification.to_user == frappe.session.user) .orderby(Notification.creation, order=Order.desc) + .limit(limit + 1) + .offset(offset) ) if only_unread: query = query.where(Notification.read == 0) - query = query.where(Notification.to_user == frappe.session.user) - result = query.run(as_dict=True) - return result + + rows = query.run(as_dict=True) + + has_next_page = len(rows) > limit + res = rows[:limit] + + frappe.response["data"] = res + frappe.response["has_next_page"] = has_next_page @frappe.whitelist() diff --git a/frappe-ui b/frappe-ui index 3dd5b3375..a302a61dc 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit 3dd5b33758a472b131b62624a1c67e752a65915d +Subproject commit a302a61dc32bfe1e868412f67dad6f7ca144254d diff --git a/frontend/index.html b/frontend/index.html index 00d34e21c..23794a969 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,12 +18,12 @@ property="og:description" content="{{ description }}" /> - {% if og_image %} + - {% if og_image %} + =3.5.0' vue-router: ^4.1.6 @@ -8219,7 +8219,7 @@ snapshots: fraction.js@4.3.7: {} - frappe-ui@0.1.212(@babel/parser@7.28.4)(@nuxt/kit@3.19.1(magicast@0.3.5))(@vue/compiler-sfc@3.5.21)(tailwindcss@3.4.17)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)): + frappe-ui@0.1.220(@babel/parser@7.28.4)(@nuxt/kit@3.19.1(magicast@0.3.5))(@vue/compiler-sfc@3.5.21)(tailwindcss@3.4.17)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)): dependencies: '@floating-ui/vue': 1.1.8(vue@3.5.21(typescript@5.9.2)) '@headlessui/vue': 1.7.23(vue@3.5.21(typescript@5.9.2)) diff --git a/frontend/src/components/CustomListRowItem.vue b/frontend/src/components/CustomListRowItem.vue index fc5575987..ec0c4d383 100644 --- a/frontend/src/components/CustomListRowItem.vue +++ b/frontend/src/components/CustomListRowItem.vue @@ -10,18 +10,11 @@ #prefix > -