diff --git a/frappe/boot.py b/frappe/boot.py index 697c3eb16e..3eeb7a868a 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -19,7 +19,7 @@ from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.form.load import get_meta_bundle from frappe.email.inbox import get_email_accounts -from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site +from frappe.integrations.frappe_providers.frappecloud_billing import current_site_info, is_fc_site from frappe.model.base_document import get_controller from frappe.permissions import has_permission from frappe.query_builder import DocType @@ -125,6 +125,8 @@ def get_bootinfo(): bootinfo.setup_wizard_completed_apps = get_setup_wizard_completed_apps() or [] bootinfo.desktop_icon_urls = get_desktop_icon_urls() bootinfo.desktop_icon_style = get_icon_style() or "Subtle" + if bootinfo.is_fc_site: + bootinfo.site_info = current_site_info() return bootinfo diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index c6bfe0b66d..6861b5f6b1 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import os +from typing import Any from rq.command import send_stop_job_command from rq.exceptions import InvalidJobOperation @@ -102,7 +103,7 @@ class DataImport(Document): self.payload_count = len(payloads) @frappe.whitelist() - def get_preview_from_template(self, import_file=None, google_sheets_url=None): + def get_preview_from_template(self, import_file: str | None = None, google_sheets_url: str | None = None): if import_file: self.import_file = import_file self.set_delimiters_flag() @@ -203,7 +204,13 @@ def start_import(data_import): @frappe.whitelist() -def download_template(doctype, export_fields=None, export_records=None, export_filters=None, file_type="CSV"): +def download_template( + doctype: str, + export_fields: str | dict[str, list[str]] | None = None, + export_records: str | None = None, + export_filters: str | dict[str, Any] | list[list[Any]] | None = None, + file_type: str = "CSV", +): """ Download template from Exporter :param doctype: Document Type diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index f74022a5f6..3ddc070fc5 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -93,15 +93,19 @@ class Importer: return # setup import log - import_log = ( - frappe.get_all( - "Data Import Log", - fields=["row_indexes", "success", "log_index"], - filters={"data_import": self.data_import.name}, - order_by="log_index", + # Only use import log for retry/resume when Data Import is persisted in DB. + # For bench data-import (CLI), the doc is never inserted, so we must not reuse logs + import_log = [] + if self.data_import.name and frappe.db.exists("Data Import", self.data_import.name): + import_log = ( + frappe.get_all( + "Data Import Log", + fields=["row_indexes", "success", "log_index"], + filters={"data_import": self.data_import.name}, + order_by="log_index", + ) + or [] ) - or [] - ) log_index = 0 diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py index ef4578f9c9..59a6102336 100644 --- a/frappe/core/doctype/deleted_document/deleted_document.py +++ b/frappe/core/doctype/deleted_document/deleted_document.py @@ -38,7 +38,7 @@ class DeletedDocument(Document): @frappe.whitelist() -def restore(name, alert=True): +def restore(name: str | int, alert: bool = True): deleted = frappe.get_doc("Deleted Document", name) if deleted.restored: @@ -69,7 +69,7 @@ def restore(name, alert=True): @frappe.whitelist() -def bulk_restore(docnames): +def bulk_restore(docnames: str | list[str]): docnames = frappe.parse_json(docnames) message = _("Restoring Deleted Document") restored, invalid, failed = [], [], [] diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index 8501be7b64..b1a6bb36be 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -130,7 +130,7 @@ def has_unseen_error_log(): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_log_doctypes(doctype, txt, searchfield, start, page_len, filters): +def get_log_doctypes(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: list): filters = filters or [] filters.extend( diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index e00fb853d5..11586aa0b5 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -6,6 +6,7 @@ import os from pathlib import Path import frappe +from frappe import _ from frappe.model.document import Document from frappe.modules.export_file import delete_folder @@ -89,6 +90,10 @@ class ModuleDef(Document): frappe.clear_cache() frappe.setup_module_map() + def before_rename(self, old, new, merge=False): + if not self.custom: + frappe.throw(_("Only Custom Modules can be renamed.")) + @frappe.whitelist() def get_installed_apps(): diff --git a/frappe/core/doctype/recorder/recorder.py b/frappe/core/doctype/recorder/recorder.py index 6102d8c736..a212910fdc 100644 --- a/frappe/core/doctype/recorder/recorder.py +++ b/frappe/core/doctype/recorder/recorder.py @@ -116,7 +116,7 @@ def serialize_request(request): @frappe.whitelist() -def add_indexes(indexes): +def add_indexes(indexes: str): frappe.only_for("Administrator") indexes = json.loads(indexes) diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 3bf470493c..5a161f1b97 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -120,7 +120,9 @@ def get_users(role): # searches for active employees @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def role_query(doctype, txt, searchfield, start, page_len, filters): +def role_query( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: list | dict | str +): return frappe.get_all( "Role", limit_start=start, diff --git a/frappe/core/doctype/rq_job/rq_job.py b/frappe/core/doctype/rq_job/rq_job.py index 02375da8bc..98ff58b84b 100644 --- a/frappe/core/doctype/rq_job/rq_job.py +++ b/frappe/core/doctype/rq_job/rq_job.py @@ -241,7 +241,7 @@ def get_all_queued_jobs(): @frappe.whitelist() -def stop_job(job_id): +def stop_job(job_id: str): frappe.get_doc("RQ Job", job_id).stop_job() diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py index 6d9207db88..f33ea63397 100644 --- a/frappe/core/doctype/sms_settings/sms_settings.py +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -46,7 +46,7 @@ def validate_receiver_nos(receiver_list): @frappe.whitelist() -def get_contact_number(contact_name, ref_doctype, ref_name): +def get_contact_number(contact_name: str, ref_doctype: str, ref_name: str): "Return mobile number of the given contact." number = frappe.db.sql( """select mobile_no, phone from tabContact @@ -62,7 +62,7 @@ def get_contact_number(contact_name, ref_doctype, ref_name): @frappe.whitelist() -def send_sms(receiver_list, msg, sender_name="", success_msg=True): +def send_sms(receiver_list: str | list[str], msg: str, sender_name: str = "", success_msg: bool = True): send_sms_hook_methods = frappe.get_hooks("send_sms") if send_sms_hook_methods: return frappe.get_attr(send_sms_hook_methods[-1])(receiver_list, msg, sender_name, success_msg) diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 9001b2893d..380d432833 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import json +from typing import Any import frappe from frappe import _ @@ -85,7 +86,7 @@ def send_user_permissions(bootinfo): @frappe.whitelist() -def get_user_permissions(user=None): +def get_user_permissions(user: str | None = None): """Get all users permissions for the user as a dict of doctype""" # if this is called from client-side, # user can access only his/her user permissions @@ -160,7 +161,9 @@ def user_permission_exists(user, allow, for_value, applicable_for=None): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters): +def get_applicable_for_doctype_list( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any] +): actual_doctype = filters.get("doctype") linked_doctypes_map = get_linked_doctypes(actual_doctype, True) @@ -192,7 +195,7 @@ def get_permitted_documents(doctype): @frappe.whitelist() -def check_applicable_doc_perm(user, doctype, docname): +def check_applicable_doc_perm(user: str, doctype: str, docname: str | int): frappe.only_for("System Manager") applicable = [] doc_exists = frappe.get_all( @@ -224,7 +227,7 @@ def check_applicable_doc_perm(user, doctype, docname): @frappe.whitelist() -def clear_user_permissions(user, for_doctype): +def clear_user_permissions(user: str, for_doctype: str): frappe.only_for("System Manager") total = frappe.db.count("User Permission", {"user": user, "allow": for_doctype}) @@ -242,7 +245,7 @@ def clear_user_permissions(user, for_doctype): @frappe.whitelist() -def add_user_permissions(data): +def add_user_permissions(data: str | dict[str, Any]): """Add and update the user permissions""" frappe.only_for("System Manager") if isinstance(data, str): diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index 046e3203f9..be839e7fcb 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -84,13 +84,14 @@ class UserType(Document): title=_("Permission Error"), ) - if not limit: - frappe.throw( + if limit is None: + frappe.msgprint( _("The limit has not set for the user type {0} in the site config file.").format( frappe.bold(self.name) ), title=_("Set Limit"), ) + return if self.user_doctypes and len(self.user_doctypes) > limit: frappe.throw( @@ -218,7 +219,9 @@ def get_non_standard_user_types(): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters): +def get_user_linked_doctypes( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | list | str +): modules = [d.get("module_name") for d in get_modules_from_app("frappe")] filters = [ @@ -254,7 +257,7 @@ def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters @frappe.whitelist() -def get_user_id(parent): +def get_user_id(parent: str): data = ( frappe.get_all( "DocField", diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.py b/frappe/desk/doctype/custom_html_block/custom_html_block.py index 35f9c3cc63..837fcb3079 100644 --- a/frappe/desk/doctype/custom_html_block/custom_html_block.py +++ b/frappe/desk/doctype/custom_html_block/custom_html_block.py @@ -27,7 +27,9 @@ class CustomHTMLBlock(Document): @frappe.whitelist() -def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filters): +def get_custom_blocks_for_user( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | str | list +): # return logged in users private blocks and all public blocks customHTMLBlock = DocType("Custom HTML Block") diff --git a/frappe/desk/doctype/desktop_layout/desktop_layout.py b/frappe/desk/doctype/desktop_layout/desktop_layout.py index 4a2308ed39..8a07b9cd77 100644 --- a/frappe/desk/doctype/desktop_layout/desktop_layout.py +++ b/frappe/desk/doctype/desktop_layout/desktop_layout.py @@ -58,6 +58,18 @@ def save_layout(user: str, layout: str, new_icons: str): return {"layout": layout} +@frappe.whitelist() +def get_layout(): + """Return the current user's saved desktop layout. Used on desk load to avoid stale cached HTML.""" + try: + doc = frappe.get_doc("Desktop Layout", frappe.session.user) + if doc.layout: + return json.loads(doc.layout) + except frappe.DoesNotExistError: + frappe.clear_last_message() + return None + + @frappe.whitelist() def delete_layout(): return frappe.delete_doc_if_exists("Desktop Layout", frappe.session.user) diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index bd8690bca0..e12e847244 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -50,7 +50,7 @@ class OnboardingStep(Document): @frappe.whitelist() -def get_onboarding_steps(ob_steps): +def get_onboarding_steps(ob_steps: str): steps = [] for s in json.loads(ob_steps): doc = frappe.get_doc("Onboarding Step", s.get("step")) diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index 4f29b8e7fc..d582988a3b 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -51,7 +51,7 @@ class SystemConsole(Document): @frappe.whitelist(methods=["POST"]) -def execute_code(doc): +def execute_code(doc: str): console = frappe.get_doc(json.loads(doc)) console.run() return console.as_dict() diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 77cdfb90db..ddcbc3eb7e 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -173,5 +173,5 @@ def has_permission(doc, ptype="read", user=None): @frappe.whitelist() -def new_todo(description): +def new_todo(description: str): frappe.get_doc({"doctype": "ToDo", "description": description}).insert() diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py index d55139b3e5..b0fe4b5399 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -195,7 +195,7 @@ def create_workspace_sidebar_for_workspaces(): @frappe.whitelist() -def add_sidebar_items(sidebar_title, sidebar_items): +def add_sidebar_items(sidebar_title: str, sidebar_items: str): sidebar_items = loads(sidebar_items) title = f"{sidebar_title}-{frappe.session.user}" w = frappe.get_doc("Workspace Sidebar", sidebar_title) diff --git a/frappe/desk/gantt.py b/frappe/desk/gantt.py index a09c52dafe..56a207f166 100644 --- a/frappe/desk/gantt.py +++ b/frappe/desk/gantt.py @@ -7,7 +7,7 @@ import frappe @frappe.whitelist() -def update_task(args, field_map): +def update_task(args: str, field_map: str): """Updates Doc (called via gantt) based on passed `field_map`""" args = frappe._dict(json.loads(args)) field_map = frappe._dict(json.loads(field_map)) diff --git a/frappe/desk/like.py b/frappe/desk/like.py index 6399673691..ecdc7d2d66 100644 --- a/frappe/desk/like.py +++ b/frappe/desk/like.py @@ -13,7 +13,7 @@ from frappe.utils import get_link_to_form @frappe.whitelist() -def toggle_like(doctype, name, add=False): +def toggle_like(doctype: str, name: str, add: str | bool = False): """Adds / removes the current user in the `__liked_by` property of the given document. If column does not exist, will add it in the database. diff --git a/frappe/desk/link_preview.py b/frappe/desk/link_preview.py index c9143ef5f1..3873daae56 100644 --- a/frappe/desk/link_preview.py +++ b/frappe/desk/link_preview.py @@ -6,7 +6,7 @@ from frappe.www.printview import set_title_values_for_link_and_dynamic_link_fiel @frappe.whitelist() @http_cache(max_age=60 * 10) -def get_preview_data(doctype, docname): +def get_preview_data(doctype: str, docname: str | int): preview_fields = [] meta = frappe.get_meta(doctype) if not meta.show_preview_popup: diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index fb5690b8a9..27a128b60c 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -80,6 +80,12 @@ margin-top: 60px; padding: 20px; } +.icons-container:has(.sidebar-card){ + margin-top: 20px; + .sidebar-card{ + gap: 6px; + } +} .modal .modal-body .icons-container,.folder-icon .icons-container { padding:0px; @@ -500,31 +506,72 @@ justify-content: center; } -.title-widget{ - display: inline-block; +.title-widget { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.5rem; + cursor: text; position: relative; } -.title-input-label{ - position: absolute; - top: 0px; - color: var(--neutral-white); - line-height: 22px; - z-index: 1; - pointers-events: none; - width: 100%; - text-align: center; -} -.title-input-wrapper{ - position: relative; - display: inline-block; - +.title-widget--read-only { + cursor: default; } -.title-input-wrapper input{ - border: 1px solid transparent; - width: 100%; - height: 100%; - background: none; +.title-widget--editable:hover .title-input-label { + opacity: 0.9; +} + +.desktop-modal-heading .title-widget--read-only .title-input-label:hover { + background-color: transparent; +} + +.desktop-modal-heading .title-widget .title-input-label { color: var(--neutral-white); + font-size: var(--text-2xl); + line-height: 1.3; + padding: 2px 4px; + border-radius: 4px; + transition: background-color 0.15s ease; +} + +.desktop-modal-heading .title-widget--editable:hover .title-input-label { + background-color: rgba(255, 255, 255, 0.08); +} + +.title-input-wrapper { + display: inline-block; + min-width: 80px; +} + +.desktop-modal-heading .title-input-wrapper .title-input { + color: var(--neutral-white); + font-size: var(--text-2xl); + line-height: 1.3; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 4px; + padding: 2px 8px; + outline: none; + min-width: 80px; + box-sizing: border-box; +} + +.desktop-modal-heading .title-input-wrapper .title-input::placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.desktop-modal-heading .title-input-wrapper .title-input:focus { + border-color: rgba(255, 255, 255, 0.5); + background-color: rgba(255, 255, 255, 0.12); +} + +.title-input-mirror { + position: absolute; + visibility: hidden; + white-space: pre; + font-size: var(--text-2xl); + font-family: inherit; + padding: 0 4px; } diff --git a/frappe/desk/page/desktop/desktop.html b/frappe/desk/page/desktop/desktop.html index 5b02b58064..bfefedb5a9 100644 --- a/frappe/desk/page/desktop/desktop.html +++ b/frappe/desk/page/desktop/desktop.html @@ -1,6 +1,6 @@
-