chore: resolve conflicts
This commit is contained in:
commit
95450c85bd
172 changed files with 61988 additions and 51409 deletions
13
.github/helper/roulette.py
vendored
13
.github/helper/roulette.py
vendored
|
|
@ -144,6 +144,14 @@ def is_frontend_code(file):
|
|||
return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue", ".html", ".svg"))
|
||||
|
||||
|
||||
def matches_postgres_filenames(files_list):
|
||||
"""Check if any changed files suggest database involvement."""
|
||||
db_keywords = ["database", "query", "schema", "postgres"]
|
||||
return any(
|
||||
any(word in f.lower() for word in db_keywords)
|
||||
for f in files_list
|
||||
)
|
||||
|
||||
def is_docs(file):
|
||||
"""Check if the file is documentation or image."""
|
||||
regex = re.compile(r"\.(md|png|jpg|jpeg|csv|svg)$|^.github|LICENSE")
|
||||
|
|
@ -174,6 +182,10 @@ if __name__ == "__main__":
|
|||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||
updated_py_file_count = len(list(filter(is_server_side_code, files_list)))
|
||||
only_py_changed = updated_py_file_count == len(files_list)
|
||||
run_postgres = (
|
||||
has_label(pr_number, "postgres", repo) or
|
||||
matches_postgres_filenames(files_list)
|
||||
)
|
||||
|
||||
# Check for Skip CI label and other conditions
|
||||
if has_skip_ci_label(pr_number, repo):
|
||||
|
|
@ -202,3 +214,4 @@ if __name__ == "__main__":
|
|||
|
||||
# If we reach here, run the build
|
||||
os.system('echo "build=strawberry" >> $GITHUB_OUTPUT')
|
||||
os.system(f'echo "run_postgres={"true" if run_postgres else "false"}" >> $GITHUB_OUTPUT')
|
||||
|
|
|
|||
7
.github/workflows/generate-pot-file.yml
vendored
7
.github/workflows/generate-pot-file.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: ["develop"]
|
||||
branch: ["develop", "version-16-hotfix"]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
|
@ -27,6 +27,11 @@ jobs:
|
|||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Run script to update POT file
|
||||
run: |
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/update_pot_file.sh
|
||||
|
|
|
|||
3
.github/workflows/server-tests.yml
vendored
3
.github/workflows/server-tests.yml
vendored
|
|
@ -28,6 +28,7 @@ jobs:
|
|||
needs: typecheck
|
||||
outputs:
|
||||
build: ${{ steps.check-build.outputs.build }}
|
||||
run_postgres: ${{ steps.check-build.outputs.run_postgres }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
|
|
@ -44,7 +45,7 @@ jobs:
|
|||
name: Tests
|
||||
uses: ./.github/workflows/_base-server-tests.yml
|
||||
with:
|
||||
enable-postgres: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }} # This enables PostgreSQL to run tests
|
||||
enable-postgres: ${{ needs.checkrun.outputs.run_postgres == 'true' }} # This enables PostgreSQL to run tests
|
||||
enable-sqlite: false # This will test against both MariaDB and SQLite if enabled
|
||||
parallel-runs: 2
|
||||
enable-coverage: ${{ github.event_name != 'pull_request' }}
|
||||
|
|
|
|||
11
.releaserc
11
.releaserc
|
|
@ -1,19 +1,22 @@
|
|||
{
|
||||
"branches": ["develop", {"name": "version-14-beta", "channel": "beta", "prerelease": true}],
|
||||
"branches": ["version-17"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular"
|
||||
"preset": "angular",
|
||||
"releaseRules": [
|
||||
{"breaking": true, "release": false}
|
||||
]
|
||||
},
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" frappe/__init__.py'
|
||||
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" frappe/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git", {
|
||||
"assets": ["frappe/__init__.py"],
|
||||
"message": "chore(release): Bumped to Version ${nextRelease.version}"
|
||||
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
|
|||
|
||||
Started in 2005, Frappe Framework was inspired by the Semantic Web. The "big idea" behind semantic web was of a framework that not only described how information is shown (like headings, body etc), but also what it means, like name, address etc.
|
||||
|
||||
By creating a web framework that allowed for easy definition of metadata, it made building complex applications easy. Applications usually designed around how users interact with a system, but not based on semantics of the underlying system. Applications built on semantics end up being much more consistent and extensible.
|
||||
By creating a web framework that allowed for easy definition of metadata, it made building complex applications easy. Applications are usually designed around how users interact with a system, but not based on semantics of the underlying system. Applications built on semantics end up being much more consistent and extensible.
|
||||
|
||||
The first application built on Framework was ERPNext, a beast with more than 700 object types. Framework is not for the light hearted - it is not the first thing you might want to learn if you are beginning to learn web programming, but if you are ready to do real work, then Framework is the right tool for the job.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ context("Customize Form", () => {
|
|||
"Set by user": "prompt",
|
||||
"By fieldname": "field:",
|
||||
Expression: "",
|
||||
"Expression (old style)": "format:",
|
||||
Random: "hash",
|
||||
"By script": "",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool =
|
|||
local.cache = {}
|
||||
local.form_dict = _dict()
|
||||
local.preload_assets = {"style": [], "script": [], "icons": []}
|
||||
local.session = _dict(user="Guest")
|
||||
local.session = _dict(user="Guest", data=_dict())
|
||||
local.dev_server = _dev_server # only for backwards compatibility
|
||||
local.qb = get_query_builder(local.conf.db_type)
|
||||
if not cache or not client_cache:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import gettext
|
|||
|
||||
import babel
|
||||
import babel.messages
|
||||
import bleach
|
||||
import nh3
|
||||
import num2words
|
||||
import pydantic
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ import frappe.boot
|
|||
import frappe.client
|
||||
import frappe.core.doctype.file.file
|
||||
import frappe.core.doctype.user.user
|
||||
import frappe.database.mariadb.database # Load database related utils
|
||||
import frappe.database.mariadb.mysqlclient # Load database related utils
|
||||
import frappe.database.query
|
||||
import frappe.desk.desktop # workspace
|
||||
import frappe.desk.form.save
|
||||
|
|
|
|||
|
|
@ -161,8 +161,8 @@ def load_desktop_data(bootinfo):
|
|||
from frappe.desk.desktop import get_workspace_sidebar_items
|
||||
|
||||
bootinfo.workspaces = get_workspace_sidebar_items()
|
||||
bootinfo.workspace_sidebar_item = get_sidebar_items()
|
||||
allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")]
|
||||
bootinfo.workspace_sidebar_item = get_sidebar_items(allowed_pages)
|
||||
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
bootinfo.app_data = []
|
||||
|
|
@ -533,7 +533,7 @@ def get_sentry_dsn():
|
|||
return os.getenv("FRAPPE_SENTRY_DSN")
|
||||
|
||||
|
||||
def get_sidebar_items():
|
||||
def get_sidebar_items(allowed_workspaces):
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import auto_generate_sidebar_from_module
|
||||
|
||||
|
|
@ -585,7 +585,7 @@ def get_sidebar_items():
|
|||
if (
|
||||
"My Workspaces" in sidebar_title
|
||||
or si.type == "Section Break"
|
||||
or w.is_item_allowed(si.link_to, si.link_type)
|
||||
or w.is_item_allowed(si.link_to, si.link_type, allowed_workspaces)
|
||||
):
|
||||
sidebar_items[sidebar_title.lower()]["items"].append(workspace_sidebar)
|
||||
add_user_specific_sidebar(sidebar_items)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import frappe.utils
|
|||
from frappe import _
|
||||
from frappe.desk.reportview import validate_args
|
||||
from frappe.desk.search import PAGE_LENGTH_FOR_LINK_VALIDATION, search_widget
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.utils import attach_expanded_links, get_safe_filters
|
||||
from frappe.utils.caching import http_cache
|
||||
|
||||
|
|
@ -420,6 +419,7 @@ def validate_link_and_fetch(
|
|||
if not docname:
|
||||
frappe.throw(_("Document Name must not be empty"))
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
fields_to_fetch = frappe.parse_json(fields_to_fetch)
|
||||
|
||||
# only cache is no fields to fetch and request is GET
|
||||
|
|
@ -427,21 +427,31 @@ def validate_link_and_fetch(
|
|||
|
||||
# Use search_widget to validate - ensures filters/custom queries are respected
|
||||
# in addition to standard permission checks
|
||||
search_args["txt"] = docname
|
||||
# we match the exact docname for non-custom queries and rely on txt for custom queries
|
||||
search_args.update(
|
||||
as_dict=False,
|
||||
# when relying on txt (custom queries), we want to match "A" with "A" only and not "A1", "BA" etc.
|
||||
# so we set page_length to a conservative value within which exact match is expected to appear
|
||||
page_length=PAGE_LENGTH_FOR_LINK_VALIDATION,
|
||||
# translated doctypes are expected to be searchable with translated values, even for custom queries
|
||||
# for non-custom queries, docname is always matched exactly so we don't translate it
|
||||
txt=_(docname) if (query and meta.translated_doctype) else docname,
|
||||
for_link_validation=True,
|
||||
)
|
||||
|
||||
search_result = frappe.call(
|
||||
search_widget,
|
||||
doctype=doctype,
|
||||
query=query,
|
||||
filters=filters,
|
||||
**search_args,
|
||||
for_link_validation=True,
|
||||
)
|
||||
|
||||
if not search_result:
|
||||
return {} # does not exist or filtered out
|
||||
|
||||
values = None
|
||||
is_virtual_dt = is_virtual_doctype(doctype)
|
||||
is_virtual_dt = bool(meta.get("is_virtual"))
|
||||
if is_virtual_dt:
|
||||
try:
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
|
|
|
|||
|
|
@ -1583,31 +1583,34 @@ def bypass_patch(context: CliCtxObj, patch_name: str, yes: bool):
|
|||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command("create-desktop-icons-and-sidebar")
|
||||
@click.command("sync-desktop-icons")
|
||||
@pass_context
|
||||
def create_icons_and_sidebar(context: CliCtxObj):
|
||||
"""Create desktop icons and workspace sidebars."""
|
||||
from frappe.desk.doctype.desktop_icon.desktop_icon import create_desktop_icons
|
||||
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import (
|
||||
create_workspace_sidebar_for_workspaces,
|
||||
)
|
||||
def sync_desktop_icons(context: CliCtxObj):
|
||||
from frappe.model.sync import import_file_by_path
|
||||
from frappe.modules.utils import get_app_level_directory_path
|
||||
from frappe.utils import update_progress_bar
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
files = []
|
||||
app_level_folders = ["desktop_icon"]
|
||||
for site in context.sites:
|
||||
print("Sycning icons for " + site)
|
||||
frappe.init(site)
|
||||
frappe.connect()
|
||||
try:
|
||||
print("Creating Desktop Icons")
|
||||
create_desktop_icons()
|
||||
print("Creating Workspace Sidebars")
|
||||
create_workspace_sidebar_for_workspaces()
|
||||
# Saving it in a command need it
|
||||
frappe.db.commit() # nosemgrep
|
||||
except Exception as e:
|
||||
print(f"Error creating icons {site}: {e}")
|
||||
finally:
|
||||
frappe.destroy()
|
||||
for app_name in frappe.get_installed_apps():
|
||||
for folder_name in app_level_folders:
|
||||
directory_path = get_app_level_directory_path(folder_name, app_name)
|
||||
if os.path.exists(directory_path):
|
||||
icon_files = [
|
||||
os.path.join(directory_path, filename) for filename in os.listdir(directory_path)
|
||||
]
|
||||
for doc_path in icon_files:
|
||||
files.append(doc_path)
|
||||
for i, doc_path in enumerate(files):
|
||||
imported = import_file_by_path(doc_path, force=True, ignore_version=True)
|
||||
if imported:
|
||||
frappe.db.commit(chain=True)
|
||||
|
||||
update_progress_bar("Updating Desktop Icons", i, len(files))
|
||||
|
||||
|
||||
commands = [
|
||||
|
|
@ -1646,5 +1649,5 @@ commands = [
|
|||
trim_database,
|
||||
clear_log_table,
|
||||
bypass_patch,
|
||||
create_icons_and_sidebar,
|
||||
sync_desktop_icons,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
order by
|
||||
if(locate(%(_txt)s, `tabContact`.full_name), locate(%(_txt)s, `tabContact`.company_name), 99999),
|
||||
`tabContact`.idx desc, `tabContact`.full_name
|
||||
limit %(start)s, %(page_len)s """,
|
||||
limit %(page_len)s offset %(start)s """,
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"_txt": txt.replace("%", ""),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"display",
|
||||
"alignment",
|
||||
"print_width",
|
||||
"width",
|
||||
"max_height",
|
||||
|
|
@ -475,6 +476,13 @@
|
|||
"max_height": "3rem",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Data\", \"Int\", \"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"fieldname": "alignment",
|
||||
"fieldtype": "Select",
|
||||
"label": "Alignment",
|
||||
"options": "\nLeft\nCenter\nRight"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class DocField(Document):
|
|||
allow_bulk_edit: DF.Check
|
||||
allow_in_quick_entry: DF.Check
|
||||
allow_on_submit: DF.Check
|
||||
alignment: DF.Literal["", "Left", "Center", "Right"]
|
||||
bold: DF.Check
|
||||
button_color: DF.Literal["", "Default", "Primary", "Info", "Success", "Warning", "Danger"]
|
||||
collapsible: DF.Check
|
||||
|
|
@ -126,7 +127,6 @@ class DocField(Document):
|
|||
|
||||
def get_link_doctype(self):
|
||||
"""Return the Link doctype for the `docfield` (if applicable).
|
||||
|
||||
* If fieldtype is Link: Return "options".
|
||||
* If fieldtype is Table MultiSelect: Return "options" of the Link field in the Child Table.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (c) {year}, {app_publisher} and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.treeview_settings["{doctype}"] = {{
|
||||
// }};
|
||||
|
|
@ -877,6 +877,9 @@ class DocType(Document):
|
|||
make_boilerplate("controller.js", self.as_dict())
|
||||
# make_boilerplate("controller_list.js", self.as_dict())
|
||||
|
||||
if self.is_tree:
|
||||
make_boilerplate("controller_tree.js", self.as_dict())
|
||||
|
||||
if self.has_web_view:
|
||||
templates_path = frappe.get_module_path(
|
||||
frappe.scrub(self.module), "doctype", frappe.scrub(self.name), "templates"
|
||||
|
|
@ -1838,34 +1841,84 @@ def validate_permissions(doctype, for_remove=False, alert=False):
|
|||
|
||||
def check_permission_dependency(d):
|
||||
if d.cancel and not d.submit:
|
||||
frappe.throw(_("{0}: Cannot set Cancel without Submit").format(get_txt(d)))
|
||||
frappe.throw(
|
||||
_("{0}: The 'Cancel' permission cannot be granted without the 'Submit' permission.").format(
|
||||
get_txt(d)
|
||||
)
|
||||
)
|
||||
|
||||
if (d.submit or d.cancel or d.amend) and not d.write:
|
||||
frappe.throw(_("{0}: Cannot set Submit, Cancel, Amend without Write").format(get_txt(d)))
|
||||
if d.amend and not d.write:
|
||||
frappe.throw(_("{0}: Cannot set Amend without Cancel").format(get_txt(d)))
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0}: The 'Submit', 'Cancel', and 'Amend' permissions cannot be granted without the 'Write' permission."
|
||||
).format(get_txt(d))
|
||||
)
|
||||
if d.amend and not d.create:
|
||||
frappe.throw(
|
||||
_("{0}: The 'Amend' permission cannot be granted without the 'Create' permission.").format(
|
||||
get_txt(d)
|
||||
)
|
||||
)
|
||||
if d.get("import") and not d.create:
|
||||
frappe.throw(_("{0}: Cannot set Import without Create").format(get_txt(d)))
|
||||
frappe.throw(
|
||||
_("{0}: The 'Import' permission cannot be granted without the 'Create' permission.").format(
|
||||
get_txt(d)
|
||||
)
|
||||
)
|
||||
|
||||
def remove_rights_for_single(d):
|
||||
if not issingle:
|
||||
return
|
||||
|
||||
if d.report:
|
||||
frappe.msgprint(_("Report cannot be set for Single types"))
|
||||
d.report = 0
|
||||
if d.get("report"):
|
||||
d.set("report", 0)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{0}: The 'Report' permission was removed because it cannot be granted for a 'single' DocType."
|
||||
).format(get_txt(d))
|
||||
)
|
||||
|
||||
if d.get("import"):
|
||||
d.set("import", 0)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{0}: The 'Import' permission was removed because it cannot be granted for a 'single' DocType."
|
||||
).format(get_txt(d))
|
||||
)
|
||||
|
||||
if d.get("export"):
|
||||
d.set("export", 0)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{0}: The 'Export' permission was removed because it cannot be granted for a 'single' DocType."
|
||||
).format(get_txt(d))
|
||||
)
|
||||
|
||||
def check_if_submittable(d):
|
||||
if d.submit and not issubmittable:
|
||||
frappe.throw(_("{0}: Cannot set Assign Submit if not Submittable").format(get_txt(d)))
|
||||
elif d.amend and not issubmittable:
|
||||
frappe.throw(_("{0}: Cannot set Assign Amend if not Submittable").format(get_txt(d)))
|
||||
if issubmittable:
|
||||
return
|
||||
|
||||
if d.submit:
|
||||
frappe.throw(
|
||||
_("{0}: The 'Submit' permission cannot be granted for a non-submittable DocType.").format(
|
||||
get_txt(d)
|
||||
)
|
||||
)
|
||||
|
||||
if d.amend:
|
||||
frappe.throw(
|
||||
_("{0}: The 'Amend' permission cannot be granted for a non-submittable DocType.").format(
|
||||
get_txt(d)
|
||||
)
|
||||
)
|
||||
|
||||
def check_if_importable(d):
|
||||
if d.get("import") and not isimportable:
|
||||
frappe.throw(_("{0}: Cannot set import as {1} is not importable").format(get_txt(d), doctype))
|
||||
frappe.throw(
|
||||
_("{0}: The 'Import' permission cannot be granted for a non-importable DocType.").format(
|
||||
get_txt(d)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_permission_for_all_role(d):
|
||||
if frappe.session.user == "Administrator":
|
||||
|
|
@ -1875,7 +1928,7 @@ def validate_permissions(doctype, for_remove=False, alert=False):
|
|||
if d.role in AUTOMATIC_ROLES:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row # {0}: Non administrator user can not set the role {1} to the custom doctype"
|
||||
"Row # {0}: Non-administrator users cannot add the role {1} to a custom DocType."
|
||||
).format(d.idx, frappe.bold(_(d.role))),
|
||||
title=_("Permissions Error"),
|
||||
)
|
||||
|
|
@ -1885,7 +1938,7 @@ def validate_permissions(doctype, for_remove=False, alert=False):
|
|||
if d.role in roles:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row # {0}: Non administrator user can not set the role {1} to the custom doctype"
|
||||
"Row # {0}: Non-administrator users cannot add the role {1} to a custom DocType."
|
||||
).format(d.idx, frappe.bold(_(d.role))),
|
||||
title=_("Permissions Error"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -892,7 +892,7 @@ def has_permission(doc, ptype=None, user=None, debug=False):
|
|||
|
||||
try:
|
||||
ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||
except ModuleNotFoundError:
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
return False
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_last_message()
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Text",
|
||||
"fieldtype": "Code",
|
||||
"label": "Error Message",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
|
|
|
|||
|
|
@ -416,9 +416,10 @@ def get_report_module_dotted_path(module, report_name):
|
|||
|
||||
def get_group_by_field(args, doctype):
|
||||
if args["aggregate_function"] == "count":
|
||||
group_by_field = "count(*) as _aggregate_column"
|
||||
group_by_field = {"COUNT": "*", "as": "_aggregate_column"}
|
||||
else:
|
||||
group_by_field = f"{args.aggregate_function}({args.aggregate_on}) as _aggregate_column"
|
||||
func_name = args["aggregate_function"].upper()
|
||||
group_by_field = {func_name: args["aggregate_on"], "as": "_aggregate_column"}
|
||||
|
||||
return group_by_field
|
||||
|
||||
|
|
|
|||
|
|
@ -786,10 +786,11 @@
|
|||
"label": "Only allow System Managers to upload public files"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-17 15:01:24.823184",
|
||||
"modified": "2026-01-02 18:13:45.430712",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ frappe.ui.form.on("User Permission", {
|
|||
frm.set_query("applicable_for", () => {
|
||||
return {
|
||||
query: "frappe.core.doctype.user_permission.user_permission.get_applicable_for_doctype_list",
|
||||
doctype: frm.doc.allow,
|
||||
filters: {
|
||||
doctype: frm.doc.allow,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -161,7 +161,8 @@ 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):
|
||||
linked_doctypes_map = get_linked_doctypes(doctype, True)
|
||||
actual_doctype = filters.get("doctype")
|
||||
linked_doctypes_map = get_linked_doctypes(actual_doctype, True)
|
||||
|
||||
linked_doctypes = []
|
||||
for linked_doctype, linked_doctype_values in linked_doctypes_map.items():
|
||||
|
|
@ -170,7 +171,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len,
|
|||
if child_doctype:
|
||||
linked_doctypes.append(child_doctype)
|
||||
|
||||
linked_doctypes += [doctype]
|
||||
linked_doctypes += [actual_doctype]
|
||||
|
||||
if txt:
|
||||
linked_doctypes = [d for d in linked_doctypes if txt.lower() in d.lower()]
|
||||
|
|
|
|||
|
|
@ -3,12 +3,137 @@
|
|||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.core.doctype.version.version import (
|
||||
_as_string,
|
||||
_generate_html_diff,
|
||||
_should_generate_html_diff,
|
||||
get_diff,
|
||||
)
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.tests.utils import make_test_objects
|
||||
|
||||
|
||||
class TestHTMLDiff(UnitTestCase):
|
||||
def test_generate_html_diff_produces_table(self):
|
||||
"""Test HTML diff generates a table with content."""
|
||||
result = _generate_html_diff("line1\nline2", "line1\nmodified")
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn("<table", result)
|
||||
self.assertIn("line1", result)
|
||||
|
||||
def test_generate_html_diff_escapes_html(self):
|
||||
"""Test HTML output is properly escaped and safe."""
|
||||
old_value = "<script>alert('xss')</script>\nline2"
|
||||
new_value = "<div>injected</div>\nline2"
|
||||
|
||||
result = _generate_html_diff(old_value, new_value)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
# Raw script/div tags should be escaped, not executable
|
||||
self.assertNotIn("<script>alert", result)
|
||||
self.assertNotIn("<div>injected", result)
|
||||
# Escaped versions should be present
|
||||
self.assertIn("<script>", result)
|
||||
self.assertIn("<div>", result)
|
||||
|
||||
def test_should_generate_html_diff_multiline(self):
|
||||
"""Test should_generate_html_diff returns True for multiline text."""
|
||||
self.assertTrue(_should_generate_html_diff("line1\nline2", "line1\nmodified"))
|
||||
self.assertTrue(_should_generate_html_diff("single", "multi\nline"))
|
||||
self.assertTrue(_should_generate_html_diff("multi\nline", "single"))
|
||||
|
||||
def test_should_generate_html_diff_long_text(self):
|
||||
"""Test should_generate_html_diff returns True for text > 80 characters."""
|
||||
self.assertTrue(_should_generate_html_diff("a" * 81, "b"))
|
||||
self.assertTrue(_should_generate_html_diff("a", "b" * 81))
|
||||
self.assertTrue(_should_generate_html_diff("a" * 81, "b" * 81))
|
||||
|
||||
def test_should_generate_html_diff_short_text(self):
|
||||
"""Test should_generate_html_diff returns False for short single-line text."""
|
||||
self.assertFalse(_should_generate_html_diff("short", "text"))
|
||||
self.assertFalse(_should_generate_html_diff("a" * 80, "b" * 80)) # Exactly 80 chars
|
||||
|
||||
def test_should_generate_html_diff_empty_values(self):
|
||||
"""Test should_generate_html_diff returns False when either value is empty."""
|
||||
self.assertFalse(_should_generate_html_diff("", "short"))
|
||||
self.assertFalse(_should_generate_html_diff("short", ""))
|
||||
self.assertFalse(_should_generate_html_diff("", ""))
|
||||
# Even long/multiline text returns False if the other value is empty
|
||||
self.assertFalse(_should_generate_html_diff("", "a" * 81))
|
||||
self.assertFalse(_should_generate_html_diff("multi\nline", ""))
|
||||
|
||||
def test_as_string_converts_values(self):
|
||||
"""Test _as_string converts values to strings correctly."""
|
||||
self.assertEqual(_as_string("text"), "text")
|
||||
self.assertEqual(_as_string(None), "")
|
||||
self.assertEqual(_as_string(""), "")
|
||||
self.assertEqual(_as_string(0), "0")
|
||||
|
||||
|
||||
class TestVersion(IntegrationTestCase):
|
||||
def test_onload_generates_html_diffs_for_multiline(self):
|
||||
"""Test onload generates HTML diffs for multiline changes."""
|
||||
version = frappe.get_doc(
|
||||
doctype="Version",
|
||||
ref_doctype="ToDo",
|
||||
docname="test-doc",
|
||||
data=frappe.as_json({"changed": [["description", "line1\nline2", "line1\nmodified"]]}),
|
||||
)
|
||||
|
||||
version.onload()
|
||||
|
||||
html_diffs = version.get_onload().get("html_diffs")
|
||||
self.assertIsNotNone(html_diffs)
|
||||
self.assertIn("description", html_diffs)
|
||||
self.assertIn("<table", html_diffs["description"])
|
||||
|
||||
def test_onload_generates_html_diffs_for_long_text(self):
|
||||
"""Test onload generates HTML diffs for text > 80 characters."""
|
||||
version = frappe.get_doc(
|
||||
doctype="Version",
|
||||
ref_doctype="ToDo",
|
||||
docname="test-doc",
|
||||
data=frappe.as_json({"changed": [["notes", "x" * 81, "y" * 81]]}),
|
||||
)
|
||||
|
||||
version.onload()
|
||||
|
||||
html_diffs = version.get_onload().get("html_diffs")
|
||||
self.assertIsNotNone(html_diffs)
|
||||
self.assertIn("notes", html_diffs)
|
||||
|
||||
def test_onload_no_html_diffs_for_simple_changes(self):
|
||||
"""Test onload doesn't generate HTML diffs for simple short changes."""
|
||||
version = frappe.get_doc(
|
||||
doctype="Version",
|
||||
ref_doctype="ToDo",
|
||||
docname="test-doc",
|
||||
data=frappe.as_json({"changed": [["status", "Open", "Closed"]]}),
|
||||
)
|
||||
|
||||
version.onload()
|
||||
|
||||
html_diffs = version.get_onload().get("html_diffs")
|
||||
self.assertIsNone(html_diffs)
|
||||
|
||||
def test_onload_handles_empty_data(self):
|
||||
"""Test onload handles empty or missing data gracefully."""
|
||||
version = frappe.get_doc(
|
||||
doctype="Version",
|
||||
ref_doctype="ToDo",
|
||||
docname="test-doc",
|
||||
data=None,
|
||||
)
|
||||
|
||||
# Should not raise an error
|
||||
version.onload()
|
||||
self.assertIsNone(version.get_onload().get("html_diffs"))
|
||||
|
||||
version.data = frappe.as_json({"changed": []})
|
||||
version.onload()
|
||||
self.assertIsNone(version.get_onload().get("html_diffs"))
|
||||
|
||||
def test_get_diff(self):
|
||||
frappe.set_user("Administrator")
|
||||
test_records = make_test_objects("Event", reset=True)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
frappe.ui.form.on("Version", "refresh", function (frm) {
|
||||
$(
|
||||
frappe.render_template("version_view", { doc: frm.doc, data: JSON.parse(frm.doc.data) })
|
||||
).appendTo(frm.fields_dict.table_html.$wrapper.empty());
|
||||
|
||||
frm.add_custom_button(__("Show all Versions"), function () {
|
||||
frappe.set_route("List", "Version", {
|
||||
ref_doctype: frm.doc.ref_doctype,
|
||||
docname: frm.doc.docname,
|
||||
frappe.ui.form.on("Version", {
|
||||
refresh: function (frm) {
|
||||
frm.add_custom_button(__("Show all Versions"), function () {
|
||||
frappe.set_route("List", "Version", {
|
||||
ref_doctype: frm.doc.ref_doctype,
|
||||
docname: frm.doc.docname,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
frm.trigger("render_version_view");
|
||||
},
|
||||
|
||||
render_version_view: async function (frm) {
|
||||
await frappe.model.with_doctype(frm.doc.ref_doctype);
|
||||
|
||||
$(
|
||||
frappe.render_template("version_view", {
|
||||
doc: frm.doc,
|
||||
data: JSON.parse(frm.doc.data),
|
||||
})
|
||||
).appendTo(frm.fields_dict.table_html.$wrapper.empty());
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import difflib
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
|
@ -74,6 +75,29 @@ class Version(Document):
|
|||
def get_data(self):
|
||||
return json.loads(self.data)
|
||||
|
||||
def onload(self):
|
||||
"""Generate HTML diffs for multiline changes on document load."""
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
data = self.get_data()
|
||||
changed = data.get("changed", [])
|
||||
if not changed:
|
||||
return
|
||||
|
||||
html_diffs = {}
|
||||
for item in changed:
|
||||
if len(item) >= 3:
|
||||
fieldname, old_str, new_str = item[0], _as_string(item[1]), _as_string(item[2])
|
||||
if not _should_generate_html_diff(old_str, new_str):
|
||||
continue
|
||||
html_diff = _generate_html_diff(old_str, new_str)
|
||||
if html_diff:
|
||||
html_diffs[fieldname] = html_diff
|
||||
|
||||
if html_diffs:
|
||||
self.set_onload("html_diffs", html_diffs)
|
||||
|
||||
|
||||
def get_diff(old, new, for_child=False, compare_cancelled=False):
|
||||
"""Get diff between 2 document objects
|
||||
|
|
@ -203,3 +227,32 @@ def get_diff(old, new, for_child=False, compare_cancelled=False):
|
|||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Version", ["ref_doctype", "docname"])
|
||||
|
||||
|
||||
def _generate_html_diff(old_str: str, new_str: str) -> str | None:
|
||||
"""Generate HTML diff for the given old and new strings."""
|
||||
old_lines = old_str.splitlines(keepends=True)
|
||||
new_lines = new_str.splitlines(keepends=True)
|
||||
|
||||
differ = difflib.HtmlDiff(wrapcolumn=80)
|
||||
html_diff = differ.make_table(
|
||||
old_lines,
|
||||
new_lines,
|
||||
fromdesc=frappe._("Original"),
|
||||
todesc=frappe._("New"),
|
||||
context=True,
|
||||
numlines=3,
|
||||
)
|
||||
return html_diff
|
||||
|
||||
|
||||
def _should_generate_html_diff(old_str: str, new_str: str) -> bool:
|
||||
"""Determine if HTML diff should be generated for the given values."""
|
||||
return (
|
||||
old_str and new_str and ("\n" in old_str or "\n" in new_str or len(old_str) > 80 or len(new_str) > 80)
|
||||
)
|
||||
|
||||
|
||||
def _as_string(value: str | None) -> str:
|
||||
"""Convert the given value to a string."""
|
||||
return cstr(value) if value is not None else ""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,52 @@
|
|||
<style>
|
||||
.version-diff-container {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.version-diff-container h5 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.version-html-diff table.diff {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.version-html-diff table.diff td {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
vertical-align: top;
|
||||
}
|
||||
.version-html-diff table.diff .diff_header {
|
||||
background-color: var(--subtle-fg);
|
||||
text-align: right;
|
||||
padding: 2px 6px;
|
||||
color: var(--text-muted);
|
||||
font-weight: normal;
|
||||
width: 40px;
|
||||
}
|
||||
.version-html-diff table.diff .diff_next {
|
||||
background-color: var(--subtle-fg);
|
||||
width: 10px;
|
||||
}
|
||||
.version-html-diff table.diff .diff_add {
|
||||
background-color: var(--diff-added);
|
||||
}
|
||||
.version-html-diff table.diff .diff_chg {
|
||||
background-color: var(--diff-changed);
|
||||
}
|
||||
.version-html-diff table.diff .diff_sub {
|
||||
background-color: var(--diff-removed);
|
||||
}
|
||||
.version-html-diff table.diff th {
|
||||
background-color: var(--subtle-fg);
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
.version-html-diff table.diff colgroup {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<div class="version-info">
|
||||
{% if data.comment %}
|
||||
<h4>{{ __("Comment") + " (" + data.comment_type }})</h4>
|
||||
|
|
@ -5,8 +54,19 @@
|
|||
{% endif %}
|
||||
|
||||
{% const getEscapedValue = (v) => v === null ? "null" : frappe.utils.escape_html(v) %}
|
||||
{% const htmlDiffs = (doc.__onload && doc.__onload.html_diffs) || {} %}
|
||||
{% if data.changed && data.changed.length %}
|
||||
<h4>{{ __("Values Changed") }}</h4>
|
||||
{% for item in data.changed %}
|
||||
{% if htmlDiffs[item[0]] %}
|
||||
<div class="version-diff-container">
|
||||
<h5>{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}</h5>
|
||||
<div class="version-html-diff">{{ htmlDiffs[item[0]] }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% var hasSimpleChanges = data.changed.some(item => !htmlDiffs[item[0]]) %}
|
||||
{% if hasSimpleChanges %}
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -17,15 +77,18 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for item in data.changed %}
|
||||
{% if !htmlDiffs[item[0]] %}
|
||||
<tr>
|
||||
<td>{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}</td>
|
||||
<td class="diff-remove">{{ getEscapedValue(item[1]) }}</td>
|
||||
<td class="diff-add">{{ getEscapedValue(item[2]) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% var _keys = ["added", "removed"]; %}
|
||||
{% for key in _keys %}
|
||||
|
|
|
|||
|
|
@ -138,11 +138,12 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
.concat(custom_rights)
|
||||
.map((r) => {
|
||||
return __(toTitle(frappe.unscrub(r)));
|
||||
});
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
$wrapper.append(`<div class="row">\
|
||||
<div class="col-xs-5"><b>${__(d.role)}</b>, ${__("Level")} ${d.permlevel || 0}</div>\
|
||||
<div class="col-xs-7">${d.rights}</div>\
|
||||
<div class="col-xs-7 text-break">${d.rights}</div>\
|
||||
</div><br>`);
|
||||
});
|
||||
}
|
||||
|
|
@ -309,6 +310,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
.attr("data-doctype", d.parent);
|
||||
|
||||
checkbox.find("label").css("text-transform", "capitalize");
|
||||
checkbox.find("label").css("align-items", "center");
|
||||
|
||||
return checkbox;
|
||||
}
|
||||
|
|
@ -415,7 +417,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
add_delete_button(row, d) {
|
||||
$(
|
||||
`<button class='btn btn-danger btn-remove-perm btn-xs'>${frappe.utils.icon(
|
||||
"delete"
|
||||
"x"
|
||||
)}</button>`
|
||||
)
|
||||
.appendTo($(`<td class="pt-4">`).appendTo(row))
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"print_hide",
|
||||
"print_hide_if_no_value",
|
||||
"print_width",
|
||||
"alignment",
|
||||
"no_copy",
|
||||
"allow_on_submit",
|
||||
"in_list_view",
|
||||
|
|
@ -302,6 +303,13 @@
|
|||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(doc.fieldtype)",
|
||||
"fieldname": "alignment",
|
||||
"fieldtype": "Select",
|
||||
"label": "Alignment",
|
||||
"options": "\nLeft\nCenter\nRight"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class CustomField(Document):
|
|||
|
||||
allow_in_quick_entry: DF.Check
|
||||
allow_on_submit: DF.Check
|
||||
alignment: DF.Literal["", "Left", "Center", "Right"]
|
||||
bold: DF.Check
|
||||
button_color: DF.Literal["", "Default", "Primary", "Info", "Success", "Warning", "Danger"]
|
||||
collapsible: DF.Check
|
||||
|
|
|
|||
|
|
@ -768,6 +768,7 @@ docfield_properties = {
|
|||
"permlevel": "Int",
|
||||
"width": "Data",
|
||||
"print_width": "Data",
|
||||
"alignment": "Select",
|
||||
"non_negative": "Check",
|
||||
"reqd": "Check",
|
||||
"unique": "Check",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
"print_hide",
|
||||
"print_hide_if_no_value",
|
||||
"print_width",
|
||||
"alignment",
|
||||
"columns",
|
||||
"width",
|
||||
"is_custom_field"
|
||||
|
|
@ -356,6 +357,13 @@
|
|||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Int', 'Float', 'Currency', 'Percent'], doc.fieldtype)",
|
||||
"fieldname": "alignment",
|
||||
"fieldtype": "Select",
|
||||
"label": "Alignment",
|
||||
"options": "\nLeft\nCenter\nRight"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.istable",
|
||||
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class CustomizeFormField(Document):
|
|||
allow_bulk_edit: DF.Check
|
||||
allow_in_quick_entry: DF.Check
|
||||
allow_on_submit: DF.Check
|
||||
alignment: DF.Literal["", "Left", "Center", "Right"]
|
||||
bold: DF.Check
|
||||
button_color: DF.Literal["", "Default", "Primary", "Info", "Success", "Warning", "Danger"]
|
||||
collapsible: DF.Check
|
||||
|
|
|
|||
|
|
@ -1637,6 +1637,24 @@ class Engine:
|
|||
meta = frappe.get_meta(doctype)
|
||||
df = meta.get_field(fieldname)
|
||||
except Exception:
|
||||
if frappe.db.db_type == "postgres":
|
||||
"""check type and accordingly choose fallback (to avoid postgres type cast errors)"""
|
||||
target_table = frappe.utils.get_table_name(doctype)
|
||||
info_schema = frappe.qb.Schema("information_schema")
|
||||
columns = info_schema.columns
|
||||
current_schema = frappe.conf.get("db_schema", "public")
|
||||
res = (
|
||||
frappe.qb.from_(columns)
|
||||
.select(columns.data_type)
|
||||
.where(
|
||||
(columns.table_name == target_table)
|
||||
& (columns.column_name == fieldname)
|
||||
& (columns.table_schema == current_schema)
|
||||
)
|
||||
).run(pluck=True)
|
||||
data_type = res[0] if res else None
|
||||
if data_type in ("smallint", "bigint", "int", "numeric"): # can add as needed
|
||||
return "0"
|
||||
return "''"
|
||||
|
||||
if df is None:
|
||||
|
|
|
|||
|
|
@ -458,14 +458,6 @@ def get_workspace_sidebar_items():
|
|||
pages = []
|
||||
private_pages = []
|
||||
|
||||
# get additional settings from Work Settings
|
||||
try:
|
||||
workspace_visibilty = loads(
|
||||
frappe.db.get_single_value("Workspace Settings", "workspace_visibility_json") or "{}"
|
||||
)
|
||||
except JSONDecodeError:
|
||||
workspace_visibilty = {}
|
||||
|
||||
# Filter Page based on Permission
|
||||
for page in all_pages:
|
||||
try:
|
||||
|
|
@ -477,9 +469,6 @@ def get_workspace_sidebar_items():
|
|||
private_pages.append(page)
|
||||
page["label"] = _(page.get("name"))
|
||||
|
||||
if page["name"] in workspace_visibilty:
|
||||
page["visibility"] = workspace_visibilty[page["name"]]
|
||||
|
||||
if not page["app"] and page["module"]:
|
||||
page["app"] = frappe.db.get_value("Module Def", page["module"], "app_name") or get_module_app(
|
||||
page["module"]
|
||||
|
|
@ -502,9 +491,6 @@ def get_workspace_sidebar_items():
|
|||
pages.append(next((x for x in all_pages if x["title"] == "Welcome Workspace"), None))
|
||||
|
||||
return {
|
||||
"workspace_setup_completed": frappe.db.get_single_value(
|
||||
"Workspace Settings", "workspace_setup_completed"
|
||||
),
|
||||
"pages": pages,
|
||||
"has_access": has_access,
|
||||
"has_create_access": frappe.has_permission(doctype="Workspace", ptype="create"),
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
else get_period(r[0], timegrain)
|
||||
for r in result
|
||||
],
|
||||
"datasets": [{"name": chart.name, "values": [r[1] for r in result]}],
|
||||
"datasets": [{"name": _(chart.name), "values": [r[1] for r in result]}],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -292,7 +292,7 @@ def get_group_by_chart_config(chart, filters) -> dict | None:
|
|||
if data:
|
||||
return {
|
||||
"labels": [item.get("name", "Not Specified") for item in data],
|
||||
"datasets": [{"name": chart.name, "values": [item["count"] for item in data]}],
|
||||
"datasets": [{"name": _(chart.name), "values": [item["count"] for item in data]}],
|
||||
}
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:label",
|
||||
"creation": "2016-02-22 03:47:45.387068",
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
{
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Icon",
|
||||
"hidden": 1,
|
||||
"label": "Icon"
|
||||
},
|
||||
{
|
||||
|
|
@ -142,7 +144,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-01-01 19:41:40.557973",
|
||||
"modified": "2026-01-25 15:29:33.884930",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desktop Icon",
|
||||
|
|
@ -160,6 +162,18 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Desk User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
|
|
|||
|
|
@ -47,25 +47,30 @@ class DesktopIcon(Document):
|
|||
def on_trash(self):
|
||||
clear_desktop_icons_cache()
|
||||
if frappe.conf.developer_mode and self.standard and self.app:
|
||||
self.delete_desktop_icon_file()
|
||||
delete_desktop_icon_file(self.app, self.label)
|
||||
|
||||
def check_for_restrict_removal(self):
|
||||
if self.restrict_removal:
|
||||
frappe.throw(_("Cannot delete Desktop Icon '{0}' as it is restricted").format(self.label))
|
||||
|
||||
def on_update(self):
|
||||
self.export_desktop_icon()
|
||||
|
||||
def after_rename(self, old, new, merge):
|
||||
delete_desktop_icon_file(self.app, old)
|
||||
self.export_desktop_icon()
|
||||
|
||||
def export_desktop_icon(self):
|
||||
allow_export = (
|
||||
self.standard and self.app and not frappe.flags.in_import and frappe.conf.developer_mode
|
||||
)
|
||||
if allow_export:
|
||||
self.export_desktop_icon()
|
||||
|
||||
def export_desktop_icon(self):
|
||||
folder_path = create_directory_on_app_path("desktop_icon", self.app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(self.label)}.json")
|
||||
doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
|
||||
strip_default_fields(self, doc_export)
|
||||
# if self.parent_icon:
|
||||
# print(self.parent_icon)
|
||||
# doc_export["parent_icon"] = frappe.db.get_value("Desktop Icon", self.parent_icon, "label")
|
||||
with open(file_path, "w+") as icon_file_doc:
|
||||
icon_file_doc.write(frappe.as_json(doc_export) + "\n")
|
||||
folder_path = create_directory_on_app_path("desktop_icon", self.app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(self.label)}.json")
|
||||
doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
|
||||
strip_default_fields(self, doc_export)
|
||||
with open(file_path, "w+") as icon_file_doc:
|
||||
icon_file_doc.write(frappe.as_json(doc_export) + "\n")
|
||||
|
||||
def delete_desktop_icon_file(self):
|
||||
folder_path = create_directory_on_app_path("desktop_icon", self.app)
|
||||
|
|
@ -81,16 +86,13 @@ class DesktopIcon(Document):
|
|||
else:
|
||||
try:
|
||||
items = bootinfo.workspace_sidebar_item[self.label.lower()]["items"]
|
||||
#
|
||||
if len(items) == 0:
|
||||
return False
|
||||
|
||||
if len(items) and all(item["type"] == "Section Break" for item in items):
|
||||
return False
|
||||
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_app_permission(self):
|
||||
for a in frappe.get_installed_apps():
|
||||
|
|
@ -121,6 +123,13 @@ class DesktopIcon(Document):
|
|||
clear_desktop_icons_cache()
|
||||
|
||||
|
||||
def delete_desktop_icon_file(app, label):
|
||||
folder_path = create_directory_on_app_path("desktop_icon", app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(label)}.json")
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
def get_workspace_names(workspaces):
|
||||
workspace_list = []
|
||||
for w in workspaces["pages"]:
|
||||
|
|
@ -150,85 +159,37 @@ def get_desktop_icons(user=None, bootinfo=None):
|
|||
"logo_url",
|
||||
"hidden",
|
||||
"name",
|
||||
"sidebar",
|
||||
]
|
||||
|
||||
active_domains = frappe.get_active_domains()
|
||||
|
||||
DocType = frappe.qb.DocType("DocType")
|
||||
if active_domains:
|
||||
blocked_condition = (
|
||||
(DocType.restrict_to_domain.isnull())
|
||||
| (DocType.restrict_to_domain == "")
|
||||
| (DocType.restrict_to_domain.notin(active_domains))
|
||||
)
|
||||
else:
|
||||
blocked_condition = (DocType.restrict_to_domain.isnull()) | (DocType.restrict_to_domain == "")
|
||||
blocked_doctypes = [
|
||||
d.get("name")
|
||||
for d in frappe.qb.from_(DocType).select(DocType.name).where(blocked_condition).run(as_dict=True)
|
||||
"restrict_removal",
|
||||
"icon_image",
|
||||
]
|
||||
|
||||
standard_icons = frappe.get_all("Desktop Icon", fields=fields, filters={"standard": 1})
|
||||
|
||||
standard_map = {}
|
||||
|
||||
for icon in standard_icons:
|
||||
if icon._doctype in blocked_doctypes:
|
||||
icon.blocked = 1
|
||||
standard_map[icon.module_name] = icon
|
||||
|
||||
user_icons = frappe.get_all("Desktop Icon", fields=fields, filters={"standard": 0, "owner": user})
|
||||
user_icons = user_icons + standard_icons
|
||||
# for icon in user_icons:
|
||||
# standard_icon = standard_map.get(icon.module_name, None)
|
||||
|
||||
# update hidden property
|
||||
for icon in user_icons:
|
||||
standard_icon = standard_map.get(icon.module_name, None)
|
||||
# # override properties from standard icon
|
||||
# if standard_icon:
|
||||
# for key in ("route", "label", "color", "icon", "link"):
|
||||
# if standard_icon.get(key):
|
||||
# icon[key] = standard_icon.get(key)
|
||||
|
||||
if icon._doctype in blocked_doctypes:
|
||||
icon.blocked = 1
|
||||
# if standard_icon.blocked:
|
||||
# icon.hidden = 1
|
||||
|
||||
# override properties from standard icon
|
||||
if standard_icon:
|
||||
for key in ("route", "label", "color", "icon", "link"):
|
||||
if standard_icon.get(key):
|
||||
icon[key] = standard_icon.get(key)
|
||||
# # flag for modules_select dialog
|
||||
# icon.hidden_in_standard = 1
|
||||
|
||||
if standard_icon.blocked:
|
||||
icon.hidden = 1
|
||||
|
||||
# flag for modules_select dialog
|
||||
icon.hidden_in_standard = 1
|
||||
|
||||
elif standard_icon.force_show:
|
||||
icon.hidden = 0
|
||||
|
||||
# add missing standard icons (added via new install apps?)
|
||||
user_icon_names = [icon.module_name for icon in user_icons]
|
||||
for standard_icon in standard_icons:
|
||||
if standard_icon.module_name not in user_icon_names:
|
||||
# if blocked, hidden too!
|
||||
if standard_icon.blocked:
|
||||
standard_icon.hidden = 1
|
||||
standard_icon.hidden_in_standard = 1
|
||||
|
||||
user_icons.append(standard_icon)
|
||||
|
||||
user_blocked_modules = frappe.get_lazy_doc("User", user).get_blocked_modules()
|
||||
for icon in user_icons:
|
||||
if icon.module_name in user_blocked_modules:
|
||||
icon.hidden = 1
|
||||
# elif standard_icon.force_show:
|
||||
# icon.hidden = 0
|
||||
|
||||
# sort by idx
|
||||
user_icons.sort(key=lambda a: a.idx)
|
||||
|
||||
# translate
|
||||
# for d in user_icons:
|
||||
# if d.label:
|
||||
# d.label = _(d.label, context=d.parent)
|
||||
# includes
|
||||
permitted_icons = []
|
||||
permitted_parent_labels = set()
|
||||
|
||||
if bootinfo:
|
||||
for s in user_icons:
|
||||
icon = frappe.get_doc("Desktop Icon", s)
|
||||
|
|
@ -263,7 +224,6 @@ def create_desktop_icons_from_workspace():
|
|||
icon.link_type = "Workspace Sidebar"
|
||||
icon.label = w.name
|
||||
icon.icon_type = "Link"
|
||||
icon.standard = 1
|
||||
icon.link_to = w.name
|
||||
icon.icon = w.icon
|
||||
if w.module:
|
||||
|
|
@ -309,7 +269,6 @@ def create_desktop_icons_from_installed_apps():
|
|||
icon = frappe.new_doc("Desktop Icon")
|
||||
icon.label = app_title
|
||||
icon.link_type = "External"
|
||||
icon.standard = 1
|
||||
icon.idx = index
|
||||
icon.icon_type = "App"
|
||||
icon.app = a
|
||||
|
|
@ -323,3 +282,23 @@ def create_desktop_icons_from_installed_apps():
|
|||
def create_desktop_icons():
|
||||
create_desktop_icons_from_installed_apps()
|
||||
create_desktop_icons_from_workspace()
|
||||
|
||||
|
||||
def create_user_icons(user, data):
|
||||
user_settings = json.loads(data)
|
||||
new_icons = user_settings.get("icons_to_create")
|
||||
if new_icons:
|
||||
new_icons = json.loads(user_settings.get("icons_to_create"))
|
||||
if new_icons:
|
||||
for icon in new_icons:
|
||||
try:
|
||||
desktop_icon = frappe.new_doc("Desktop Icon")
|
||||
desktop_icon.update(icon)
|
||||
desktop_icon.owner = user
|
||||
desktop_icon.save()
|
||||
except Exception as e:
|
||||
frappe.log_error("Error in syncing icons", e)
|
||||
user_settings.pop("icons_to_create", None)
|
||||
frappe.cache.hset("_user_settings", f"{'Desktop Icon'}::{user}", json.dumps(user_settings))
|
||||
return json.dumps(user_settings)
|
||||
return data
|
||||
|
|
|
|||
8
frappe/desk/doctype/desktop_layout/desktop_layout.js
Normal file
8
frappe/desk/doctype/desktop_layout/desktop_layout.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2026, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Desktop Layout", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
67
frappe/desk/doctype/desktop_layout/desktop_layout.json
Normal file
67
frappe/desk/doctype/desktop_layout/desktop_layout.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:user",
|
||||
"creation": "2026-01-18 02:17:17.304705",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"layout"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "layout",
|
||||
"fieldtype": "Code",
|
||||
"label": "Layout",
|
||||
"options": "JSON"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-25 15:30:12.805037",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desktop Layout",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Desk User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
50
frappe/desk/doctype/desktop_layout/desktop_layout.py
Normal file
50
frappe/desk/doctype/desktop_layout/desktop_layout.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright (c) 2026, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DesktopLayout(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
layout: DF.Code | None
|
||||
user: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_layout(user, layout, new_icons):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
layout = json.loads(layout)
|
||||
new_icons = json.loads(new_icons)
|
||||
desktop_layout = None
|
||||
try:
|
||||
desktop_layout = frappe.get_doc("Desktop Layout", frappe.session.user)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_last_message()
|
||||
desktop_layout = frappe.new_doc("Desktop Layout")
|
||||
desktop_layout.user = frappe.session.user
|
||||
|
||||
if layout:
|
||||
desktop_layout.layout = json.dumps(layout)
|
||||
desktop_layout.save()
|
||||
|
||||
for icon in new_icons:
|
||||
desktop_icon = frappe.new_doc("Desktop Icon")
|
||||
desktop_icon.update(icon)
|
||||
desktop_icon.owner = frappe.session.user
|
||||
desktop_icon.save()
|
||||
|
||||
return {"layout": layout}
|
||||
20
frappe/desk/doctype/desktop_layout/test_desktop_layout.py
Normal file
20
frappe/desk/doctype/desktop_layout/test_desktop_layout.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright (c) 2026, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestDesktopLayout(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for DesktopLayout.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
@ -307,7 +307,8 @@ def new_page(new_page):
|
|||
# add to workspace sidebar items
|
||||
if not doc.public:
|
||||
add_to_my_workspace(doc)
|
||||
return {"workspace_pages": get_workspace_sidebar_items(), "sidebar_items": get_sidebar_items()}
|
||||
workspaces = get_workspace_sidebar_items()
|
||||
return {"workspace_pages": workspaces, "sidebar_items": get_sidebar_items(workspaces)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestWorkspaceSettings(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright (c) 2024, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Workspace Settings", {
|
||||
setup(frm) {
|
||||
frm.hide_full_form_button = true;
|
||||
frm.docfields = [];
|
||||
frm.workspace_map = {};
|
||||
let workspace_visibilty = JSON.parse(frm.doc.workspace_visibility_json || "{}");
|
||||
|
||||
// build fields from workspaces
|
||||
let cnt = 0,
|
||||
column_added = false;
|
||||
for (let page of frappe.boot.allowed_workspaces) {
|
||||
if (page.public) {
|
||||
frm.workspace_map[page.name] = page;
|
||||
cnt++;
|
||||
frm.docfields.push({
|
||||
fieldtype: "Check",
|
||||
fieldname: page.name,
|
||||
label: page.title + (page.parent_page ? ` (${page.parent_page})` : ""),
|
||||
initial_value: workspace_visibilty[page.name] !== 0, // not set is also visible
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frappe.temp = frm;
|
||||
},
|
||||
validate(frm) {
|
||||
frm.doc.workspace_visibility_json = JSON.stringify(frm.dialog.get_values());
|
||||
frm.doc.workspace_setup_completed = 1;
|
||||
},
|
||||
after_save(frm) {
|
||||
// reload page to show latest sidebar
|
||||
frappe.app.sidebar.reload();
|
||||
},
|
||||
});
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-08-02 14:20:30.177818",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"select_workspaces_section",
|
||||
"workspace_visibility_json",
|
||||
"workspace_setup_completed"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "select_workspaces_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Select Workspaces"
|
||||
},
|
||||
{
|
||||
"fieldname": "workspace_visibility_json",
|
||||
"fieldtype": "JSON",
|
||||
"in_list_view": 1,
|
||||
"label": "Workspace Visibility",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "workspace_setup_completed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Workspace Setup Completed"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-03 21:29:54.127014",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Workspace Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class WorkspaceSettings(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
workspace_setup_completed: DF.Check
|
||||
workspace_visibility_json: DF.JSON
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
def on_update(self):
|
||||
frappe.clear_cache()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_sequence(sidebar_items):
|
||||
if not WorkspaceSettings("Workspace Settings").has_permission():
|
||||
frappe.throw_permission_error()
|
||||
|
||||
cnt = 1
|
||||
for item in json.loads(sidebar_items):
|
||||
frappe.db.set_value("Workspace", item.get("name"), "sequence_id", cnt)
|
||||
frappe.db.set_value("Workspace", item.get("name"), "parent_page", item.get("parent") or "")
|
||||
cnt += 1
|
||||
|
||||
frappe.clear_cache()
|
||||
frappe.toast(frappe._("Updated"))
|
||||
|
|
@ -50,34 +50,31 @@ class WorkspaceSidebar(Document):
|
|||
self.user.build_permissions()
|
||||
|
||||
def before_save(self):
|
||||
allow_export = self.app and not frappe.flags.in_import and frappe.conf.developer_mode
|
||||
if allow_export:
|
||||
self.export_sidebar()
|
||||
self.export_sidebar()
|
||||
self.set_module()
|
||||
|
||||
def export_sidebar(self):
|
||||
folder_path = create_directory_on_app_path("workspace_sidebar", self.app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(self.title)}.json")
|
||||
doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
|
||||
doc_export = strip_default_fields(self, doc_export)
|
||||
with open(file_path, "w+") as doc_file:
|
||||
doc_file.write(frappe.as_json(doc_export) + "\n")
|
||||
|
||||
def delete_file(self):
|
||||
folder_path = create_directory_on_app_path("workspace_sidebar", self.app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(self.title)}.json")
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
allow_export = self.app and not frappe.flags.in_import and frappe.conf.developer_mode
|
||||
if allow_export:
|
||||
folder_path = create_directory_on_app_path("workspace_sidebar", self.app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(self.title)}.json")
|
||||
doc_export = self.as_dict(no_nulls=True, no_private_properties=True)
|
||||
doc_export = strip_default_fields(self, doc_export)
|
||||
with open(file_path, "w+") as doc_file:
|
||||
doc_file.write(frappe.as_json(doc_export) + "\n")
|
||||
|
||||
def on_trash(self):
|
||||
if is_workspace_manager():
|
||||
if frappe.conf.developer_mode and self.app:
|
||||
self.delete_file()
|
||||
self.delete_desktop_icon()
|
||||
delete_file(self.app, self.title)
|
||||
else:
|
||||
frappe.throw(_("You need to be Workspace Manager to delete a public workspace."))
|
||||
|
||||
def is_item_allowed(self, name, item_type):
|
||||
def after_rename(self, old, new, merge):
|
||||
delete_file(self.app, old)
|
||||
self.export_sidebar()
|
||||
|
||||
def is_item_allowed(self, name, item_type, allowed_workspaces):
|
||||
if frappe.session.user == "Administrator":
|
||||
return True
|
||||
|
||||
|
|
@ -100,12 +97,7 @@ class WorkspaceSidebar(Document):
|
|||
if item_type == "url":
|
||||
return True
|
||||
if item_type == "workspace":
|
||||
try:
|
||||
workspace = frappe.get_cached_doc("Workspace", name)
|
||||
if workspace.module in self.allowed_modules:
|
||||
return True
|
||||
except frappe.DoesNotExistError:
|
||||
return False
|
||||
return name in allowed_workspaces
|
||||
|
||||
def get_cached(self, cache_key, fallback_fn):
|
||||
value = frappe.cache.get_value(cache_key, user=frappe.session.user)
|
||||
|
|
@ -137,16 +129,6 @@ class WorkspaceSidebar(Document):
|
|||
if counts and counts.most_common(1)[0]:
|
||||
return counts.most_common(1)[0][0]
|
||||
|
||||
def delete_desktop_icon(self):
|
||||
desktop_icon = frappe.get_all(
|
||||
"Desktop Icon",
|
||||
filters=[{"link_type": "Workspace Sidebar"}, {"link_to": self.name}],
|
||||
limit=1,
|
||||
pluck="name",
|
||||
)
|
||||
if desktop_icon:
|
||||
frappe.delete_doc("Desktop Icon", desktop_icon[0])
|
||||
|
||||
def get_allowed_modules(self):
|
||||
if not self.user.allow_modules:
|
||||
self.user.build_permissions()
|
||||
|
|
@ -154,6 +136,13 @@ class WorkspaceSidebar(Document):
|
|||
return self.user.allow_modules
|
||||
|
||||
|
||||
def delete_file(app, title):
|
||||
folder_path = create_directory_on_app_path("workspace_sidebar", app)
|
||||
file_path = os.path.join(folder_path, f"{frappe.scrub(title)}.json")
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
def is_workspace_manager():
|
||||
return "Workspace Manager" in frappe.get_roles()
|
||||
|
||||
|
|
@ -339,7 +328,7 @@ def choose_top_doctypes(doctype_names):
|
|||
try:
|
||||
doctype_count_map = {}
|
||||
for doctype in doctype_names:
|
||||
if not is_single_doctype(doctype):
|
||||
if not is_single_doctype(doctype) and not frappe.get_meta(doctype).is_virtual:
|
||||
doctype_count_map[doctype] = frappe.db.count(doctype)
|
||||
top_doctypes = [
|
||||
name
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
|
|
@ -8,6 +11,9 @@ from frappe.model import log_types
|
|||
from frappe.query_builder import DocType
|
||||
from frappe.utils import get_url_to_form
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_follow(doctype: str, doc_name: str, following: bool):
|
||||
|
|
@ -18,7 +24,7 @@ def update_follow(doctype: str, doc_name: str, following: bool):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def follow_document(doctype, doc_name, user):
|
||||
def follow_document(doctype: str, doc_name: str, user: str) -> Document | bool:
|
||||
"""
|
||||
param:
|
||||
Doctype name
|
||||
|
|
@ -67,7 +73,7 @@ def follow_document(doctype, doc_name, user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def unfollow_document(doctype, doc_name, user):
|
||||
def unfollow_document(doctype: str, doc_name: str, user: str) -> bool:
|
||||
doc = frappe.get_all(
|
||||
"Document Follow",
|
||||
filters={"ref_doctype": doctype, "ref_docname": doc_name, "user": user},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
--folder-icon-background-color: var(--surface-gray-1);
|
||||
--desktop-modal-radius: 30px;
|
||||
--desktop-icon-line-height: 115%;
|
||||
--navbar-height: 52px;
|
||||
}
|
||||
[data-theme="dark"]{
|
||||
--folder-icon-background-color: #2b2b2b;
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
width: 100%;
|
||||
padding: 10px 20px 10px 20px;
|
||||
box-sizing: border-box;
|
||||
height: 52px;
|
||||
height: var(--navbar-height);
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 1030;
|
||||
|
|
@ -103,7 +104,7 @@
|
|||
padding: 13px 16px 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
.desktop-icon.edit-mode .hide-button {
|
||||
.desktop-icon.desktop-edit-mode .hide-button {
|
||||
display: flex;
|
||||
}
|
||||
.icon-container:has(.app-logo) {
|
||||
|
|
@ -182,6 +183,7 @@
|
|||
& .modal-content {
|
||||
top: 120px;
|
||||
border-radius: var(--desktop-modal-radius);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -195,10 +197,13 @@
|
|||
width: var(--desktop-modal-width);
|
||||
height: var(--desktop-modal-height);
|
||||
padding: 24px 23px !important;
|
||||
width: fit-content;
|
||||
& .icons{
|
||||
gap: 0px 0px;
|
||||
}
|
||||
& .icons:has(.desktop-edit-mode){
|
||||
margin-top: 4px;
|
||||
gap: 6px 6px;
|
||||
}
|
||||
.icon-container{
|
||||
min-height: var(--desktop-icon-dimension);
|
||||
}
|
||||
|
|
@ -234,7 +239,7 @@
|
|||
}
|
||||
.modal-body .icons{
|
||||
margin-top: 0px;
|
||||
place-self: start;
|
||||
place-self: anchor-center;
|
||||
& .desktop-icon{
|
||||
height: 126px;
|
||||
width: 127px;
|
||||
|
|
@ -337,7 +342,7 @@
|
|||
left: 108px;
|
||||
}
|
||||
|
||||
.edit-mode{
|
||||
.desktop-edit-mode{
|
||||
border: 1px dashed var(--outline-gray-2);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
|
@ -395,7 +400,9 @@
|
|||
}
|
||||
|
||||
.desktop-modal-body {
|
||||
width: 90vw;
|
||||
width: calc(100vw - 20px);
|
||||
padding-left: 0px !important;
|
||||
padding-right: 0px !important;
|
||||
> .icons-container {
|
||||
width: 100%;
|
||||
overflow: hidden !important;
|
||||
|
|
@ -404,10 +411,8 @@
|
|||
padding: 0;
|
||||
|
||||
> .icons {
|
||||
position: relative;
|
||||
right: 6%;
|
||||
column-gap: 4px;
|
||||
row-gap: 8px;
|
||||
column-gap: 6px;
|
||||
row-gap: 6px;
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
--desktop-icon-container: 100px;
|
||||
|
|
@ -463,4 +468,69 @@
|
|||
[data-theme="dark"] .desktop-edit:hover{
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.desktop-navbar-modal-search:hover{
|
||||
outline: 1px solid var(--surface-gray-3);
|
||||
}
|
||||
.desktop-pane{
|
||||
position: absolute;
|
||||
top: var(--navbar-height);
|
||||
right: 0px;
|
||||
width: 300px;
|
||||
border-left: 1px solid var(--border-color);
|
||||
background-color: rgba(255,255,255, 1);
|
||||
height: 100vh;
|
||||
}
|
||||
.pane-header{
|
||||
display: flex;
|
||||
font-size: var(--text-lg);
|
||||
padding: var(--padding-md);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.pane-icons-area{
|
||||
.icons-container {
|
||||
height: 100%;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.icons{
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-icon{
|
||||
cursor: pointer;
|
||||
gap: 0px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-widget{
|
||||
display: inline-block;
|
||||
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-input-wrapper input{
|
||||
border: 1px solid transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
|
|
@ -53,7 +53,6 @@
|
|||
<div class="desktop-avatar">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
<div class="desktop-container">
|
||||
|
|
@ -66,4 +65,5 @@
|
|||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden" id="desktop-layout">{{ desktop_layout }}</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
frappe.desktop_utils = {};
|
||||
frappe.desktop_grids = [];
|
||||
frappe.desktop_icons_objects = [];
|
||||
frappe.new_icons = [];
|
||||
$.extend(frappe.desktop_utils, {
|
||||
modal: null,
|
||||
modal_stack: [],
|
||||
|
|
@ -42,6 +43,9 @@ function get_route(desktop_icon) {
|
|||
let item = {};
|
||||
if (desktop_icon.link_type == "External" && desktop_icon.link) {
|
||||
route = window.location.origin + desktop_icon.link;
|
||||
if (desktop_icon.link.startsWith("http") || desktop_icon.link.startsWith("https")) {
|
||||
route = desktop_icon.link;
|
||||
}
|
||||
} else {
|
||||
let sidebar = frappe.boot.workspace_sidebar_item[desktop_icon.label.toLowerCase()];
|
||||
if (desktop_icon.link_type == "Workspace Sidebar" && sidebar) {
|
||||
|
|
@ -87,6 +91,9 @@ function get_route(desktop_icon) {
|
|||
type: first_link.link_type,
|
||||
name: first_link.link_to,
|
||||
tab: first_link.tab,
|
||||
route_options: {
|
||||
sidebar: desktop_icon.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -95,19 +102,18 @@ function get_route(desktop_icon) {
|
|||
return route;
|
||||
}
|
||||
|
||||
function get_desktop_icon_by_label(title, filters) {
|
||||
function get_desktop_icon_by_label(title, filters, force) {
|
||||
if (force === undefined) force = false;
|
||||
let icons = frappe.desktop_icons;
|
||||
if (frappe.pages["desktop"].desktop_page.edit_mode) {
|
||||
if (!force && frappe.pages["desktop"].desktop_page.edit_mode) {
|
||||
icons = frappe.new_desktop_icons;
|
||||
}
|
||||
if (!filters) {
|
||||
return icons.find((f) => f.label === title && f.hidden != 1);
|
||||
return icons.find((f) => f.label === title);
|
||||
} else {
|
||||
return icons.find((f) => {
|
||||
return (
|
||||
f.label === title &&
|
||||
Object.keys(filters).every((key) => f[key] === filters[key]) &&
|
||||
f.hidden != 1
|
||||
f.label === title && Object.keys(filters).every((key) => f[key] === filters[key])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -119,50 +125,71 @@ function get_desktop_icon_by_idx(idx, parent_icon) {
|
|||
|
||||
function save_desktop(icons) {
|
||||
// saving in localStorage;
|
||||
localStorage.setItem(`${frappe.session.user}:desktop`, JSON.stringify(icons));
|
||||
frappe.toast("Desktop Saved");
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
frappe.pages["desktop"].desktop_page.save_layout(icons, frappe.new_icons);
|
||||
}
|
||||
|
||||
function reset_to_default() {
|
||||
localStorage.setItem(`${frappe.session.user}:desktop`, null);
|
||||
frappe.db.delete_doc("Desktop Layout", frappe.session.user).then(() => {
|
||||
frappe.ui.toolbar.clear_cache();
|
||||
});
|
||||
}
|
||||
|
||||
frappe.pages["desktop"].on_page_show = function () {
|
||||
frappe.pages["desktop"].desktop_page.setup();
|
||||
};
|
||||
|
||||
function toggle_icons(icons) {
|
||||
icons.forEach((i) => {
|
||||
$(i).parent().parent().show();
|
||||
});
|
||||
}
|
||||
|
||||
frappe.desktop_utils.get_folder_icons = function (folder_name) {
|
||||
let icons_in_folder = [];
|
||||
let icons = frappe.desktop_icons;
|
||||
if (frappe.pages["desktop"].desktop_page.edit_mode) {
|
||||
icons = frappe.new_desktop_icons;
|
||||
}
|
||||
icons.forEach((icon) => {
|
||||
if (icon.parent_icon == folder_name) {
|
||||
icons_in_folder.push(icon.label);
|
||||
}
|
||||
});
|
||||
return icons_in_folder;
|
||||
};
|
||||
|
||||
function add_icons_to_folder(folder_name, items) {
|
||||
let folder = get_desktop_icon_by_label(folder_name);
|
||||
items.forEach((item) => {
|
||||
let icon = get_desktop_icon_by_label(item);
|
||||
icon.parent_icon = folder.label;
|
||||
});
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
}
|
||||
|
||||
class DesktopPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.edit_mode = false;
|
||||
this.prepare();
|
||||
this.make(page);
|
||||
this.setup_events();
|
||||
}
|
||||
update() {
|
||||
this.prepare();
|
||||
this.make();
|
||||
this.make(this.page);
|
||||
this.setup();
|
||||
}
|
||||
update() {
|
||||
this.make(this.page);
|
||||
this.setup();
|
||||
}
|
||||
|
||||
prepare() {
|
||||
this.apps_icons = [];
|
||||
|
||||
this.hidden_icons = [];
|
||||
this.folders = [];
|
||||
const icon_map = {};
|
||||
frappe.desktop_icons = this.get_saved_layout() || frappe.boot.desktop_icons;
|
||||
let icons = this.edit_mode ? frappe.new_desktop_icons : frappe.desktop_icons;
|
||||
const all_icons = icons.filter((icon) => {
|
||||
if (icon.hidden != 1) {
|
||||
icon.child_icons = [];
|
||||
icon_map[icon.label] = icon;
|
||||
if (icon.icon_type == "Folder") {
|
||||
this.folders.push(icon.label);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
this.hidden_icons.push(icon);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
|
@ -183,20 +210,43 @@ class DesktopPage {
|
|||
}
|
||||
return JSON.parse(localStorage.getItem(`${frappe.session.user}:desktop`));
|
||||
}
|
||||
setup_events() {
|
||||
this.wrapper.find(".hide-button").on("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
let desktop_label = event.currentTarget.parentElement.dataset.id;
|
||||
let desktop_icon = get_desktop_icon_by_label(desktop_label);
|
||||
desktop_icon.hidden = 1;
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
sync_layout() {
|
||||
const me = this;
|
||||
let saved_layout = JSON.parse(localStorage.getItem(`${frappe.session.user}:desktop`));
|
||||
if (!this.data && saved_layout) {
|
||||
this.save_layout(saved_layout);
|
||||
} else if (Object.keys(this.data).length != 0) {
|
||||
frappe.desktop_icons = this.data;
|
||||
} else {
|
||||
frappe.desktop_icons = frappe.boot.desktop_icons;
|
||||
}
|
||||
}
|
||||
save_layout(layout, new_icons) {
|
||||
const me = this;
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.desktop_layout.desktop_layout.save_layout",
|
||||
args: {
|
||||
user: frappe.session.user,
|
||||
layout: JSON.stringify(layout),
|
||||
new_icons: JSON.stringify(new_icons),
|
||||
},
|
||||
callback: function (r) {
|
||||
me.data = r.message.layout;
|
||||
me.make(me.page);
|
||||
me.setup();
|
||||
frappe.new_icons = [];
|
||||
},
|
||||
});
|
||||
}
|
||||
make() {
|
||||
this.page.page_head.hide();
|
||||
$(this.page.body).empty();
|
||||
$(frappe.render_template("desktop")).appendTo(this.page.body);
|
||||
if (!this.data) {
|
||||
this.data = JSON.parse($("#desktop-layout").text());
|
||||
}
|
||||
this.sync_layout();
|
||||
this.prepare();
|
||||
this.wrapper = this.page.body.find(".desktop-container");
|
||||
this.icon_grid = new DesktopIconGrid({
|
||||
wrapper: this.wrapper,
|
||||
|
|
@ -206,6 +256,7 @@ class DesktopPage {
|
|||
col: 3,
|
||||
},
|
||||
});
|
||||
this.setup_context_menu();
|
||||
if (this.edit_mode) {
|
||||
this.start_editing_layout();
|
||||
}
|
||||
|
|
@ -217,9 +268,10 @@ class DesktopPage {
|
|||
this.setup_navbar();
|
||||
this.setup_awesomebar();
|
||||
this.handle_route_change();
|
||||
this.setup_events();
|
||||
this.setup_edit_button();
|
||||
}
|
||||
setup_edit_button() {
|
||||
if (this.edit_mode || frappe.is_mobile()) return;
|
||||
const me = this;
|
||||
$(".desktop-edit").remove();
|
||||
this.$desktop_edit_button = $(
|
||||
|
|
@ -233,13 +285,14 @@ class DesktopPage {
|
|||
me.start_editing_layout();
|
||||
});
|
||||
}
|
||||
setup_editing_mode() {
|
||||
setup_context_menu() {
|
||||
const me = this;
|
||||
let menu_items = [
|
||||
{
|
||||
label: "Edit Layout",
|
||||
icon: "edit",
|
||||
onClick: function () {
|
||||
me.$desktop_edit_button.hide();
|
||||
frappe.new_desktop_icons = JSON.parse(JSON.stringify(frappe.desktop_icons));
|
||||
me.start_editing_layout();
|
||||
},
|
||||
|
|
@ -261,23 +314,28 @@ class DesktopPage {
|
|||
}
|
||||
stop_editing_layout(action) {
|
||||
this.edit_mode = false;
|
||||
|
||||
$(".desktop-icon").removeClass("edit-mode");
|
||||
$(".desktop-icon").not(".folder-icon .desktop-icon").removeClass("desktop-edit-mode");
|
||||
$(".desktop-wrapper").removeAttr("data-mode");
|
||||
$(".add-new-icon").remove();
|
||||
this.desktop_pane.hide();
|
||||
if (action === "cancel") {
|
||||
frappe.new_desktop_icons = null;
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
|
||||
// submit
|
||||
save_desktop(frappe.new_desktop_icons);
|
||||
}
|
||||
|
||||
start_editing_layout() {
|
||||
this.edit_mode = true;
|
||||
$(".desktop-icon").addClass("edit-mode");
|
||||
const me = this;
|
||||
this.desktop_pane = new IconsPane();
|
||||
$(".desktop-wrapper").attr("data-mode", "Edit");
|
||||
$(".desktop-edit").remove();
|
||||
frappe.desktop_icons_objects.forEach((icon) => {
|
||||
icon.edit_mode = true;
|
||||
});
|
||||
frappe.desktop_grids.forEach((desktop_grid) => {
|
||||
if (!desktop_grid.no_dragging) {
|
||||
desktop_grid.grids.forEach((grid) => {
|
||||
|
|
@ -285,16 +343,42 @@ class DesktopPage {
|
|||
});
|
||||
}
|
||||
});
|
||||
frappe.desktop_icons_objects.forEach((icon_object) => {
|
||||
icon_object.setup_dragging();
|
||||
this.add_new_icons_to_grid();
|
||||
if (this.edit_mode) {
|
||||
this.setup_edit_buttons();
|
||||
this.desktop_pane.show();
|
||||
}
|
||||
}
|
||||
add_new_icons_to_grid() {
|
||||
let grid = $($(".desktop-container .icons").get(0));
|
||||
this.add_new_icon = `<div class="desktop-icon desktop-edit-mode add-new-icon" title="Add New Icon">
|
||||
${frappe.utils.icon("plus", "lg")}
|
||||
New Icon
|
||||
</div>`;
|
||||
grid.append(this.add_new_icon);
|
||||
$(".add-new-icon").on("click", function () {
|
||||
frappe.ui.form.make_quick_entry(
|
||||
"Desktop Icon",
|
||||
function (icon) {
|
||||
frappe.new_desktop_icons.push(icon);
|
||||
frappe.new_icons.push(icon);
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
},
|
||||
"",
|
||||
"",
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
});
|
||||
if (this.edit_mode) this.setup_edit_buttons();
|
||||
}
|
||||
setup_edit_buttons() {
|
||||
const me = this;
|
||||
this.$edit_button = $(".edit-mode-buttons");
|
||||
this.$edit_button.find(".discard").on("click", function () {
|
||||
me.stop_editing_layout("cancel");
|
||||
me.delete_new_icons();
|
||||
$($(".desktop-container .icons").get(0)).find(".add-new-icon").remove();
|
||||
});
|
||||
this.$edit_button.find(".save").on("click", function () {
|
||||
me.stop_editing_layout("submit");
|
||||
|
|
@ -306,6 +390,11 @@ class DesktopPage {
|
|||
full_height: false,
|
||||
});
|
||||
}
|
||||
|
||||
delete_new_icons() {
|
||||
frappe.new_icons = [];
|
||||
}
|
||||
|
||||
setup_avatar() {
|
||||
$(".desktop-avatar").html(frappe.avatar(frappe.session.user, "avatar-medium"));
|
||||
let is_dark = document.documentElement.getAttribute("data-theme") === "dark";
|
||||
|
|
@ -403,41 +492,19 @@ class DesktopPage {
|
|||
me.edit_mode = false;
|
||||
$(".desktop-icon").removeClass("edit-mode");
|
||||
$(".desktop-wrapper").removeAttr("data-mode");
|
||||
$(".desktop-edit").remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// setup_icon_search() {
|
||||
// let all_icons = $(".icon-title");
|
||||
// let icons_to_show = [];
|
||||
// $(".desktop-container .icons").append(
|
||||
// "<div class='no-apps-message hidden'> No apps found </div>"
|
||||
// );
|
||||
// $(".desktop-search-wrapper > #navbar-search").on("input", function (e) {
|
||||
// let search_query = $(e.target).val().toLowerCase();
|
||||
// console.log(search_query);
|
||||
// icons_to_show = [];
|
||||
// all_icons.each(function (index, element) {
|
||||
// $(element).parent().parent().hide();
|
||||
// let label = $(element).text().toLowerCase();
|
||||
// if (label.includes(search_query)) {
|
||||
// icons_to_show.push(element);
|
||||
// }
|
||||
// });
|
||||
|
||||
// if (icons_to_show.length == 0) {
|
||||
// $(".desktop-container .icons").find(".no-apps-message").removeClass("hidden");
|
||||
// } else {
|
||||
// $(".desktop-container .icons").find(".no-apps-message").addClass("hidden");
|
||||
// }
|
||||
// toggle_icons(icons_to_show);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
class DesktopIconGrid {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
this.init();
|
||||
}
|
||||
static folder_count = 0;
|
||||
init() {
|
||||
this.icons = [];
|
||||
this.icons_html = [];
|
||||
// this.page_size = {
|
||||
|
|
@ -452,7 +519,16 @@ class DesktopIconGrid {
|
|||
this.make();
|
||||
frappe.desktop_grids.push(this);
|
||||
}
|
||||
|
||||
add_folder() {
|
||||
DesktopIconGrid.folder_count++;
|
||||
let icon = frappe.model.get_new_doc("Desktop Icon");
|
||||
icon.icon_type = "Folder";
|
||||
icon.label = `Untitled ${DesktopIconGrid.folder_count}`;
|
||||
icon.idx = 100000;
|
||||
frappe.new_desktop_icons.push(icon);
|
||||
frappe.new_icons.push(icon);
|
||||
return icon;
|
||||
}
|
||||
prepare() {
|
||||
this.total_pages = 1;
|
||||
this.icons_data = this.icons_data.sort((a, b) => {
|
||||
|
|
@ -467,6 +543,9 @@ class DesktopIconGrid {
|
|||
make() {
|
||||
const me = this;
|
||||
this.icons_container = $(`<div class="icons-container"></div>`).appendTo(this.wrapper);
|
||||
if (this.compact) {
|
||||
this.icons_container.css("margin-top", "0px");
|
||||
}
|
||||
for (let i = 0; i < this.total_pages; i++) {
|
||||
let template = `<div class="icons"></div>`;
|
||||
|
||||
|
|
@ -478,9 +557,6 @@ class DesktopIconGrid {
|
|||
}
|
||||
this.grids.push($(template).appendTo(this.icons_container));
|
||||
this.make_icons(this.icons_data_by_page, this.grids[i]);
|
||||
// if (!this.no_dragging) {
|
||||
// this.setup_reordering(this.grids[i]);
|
||||
// }
|
||||
}
|
||||
if (!this.in_folder && this.total_pages > 1) {
|
||||
this.add_page_indicators();
|
||||
|
|
@ -605,19 +681,31 @@ class DesktopIconGrid {
|
|||
}
|
||||
make_icons(icons_data, grid) {
|
||||
icons_data.forEach((icon) => {
|
||||
let icon_obj = new DesktopIcon(icon, this.in_folder);
|
||||
let icon_obj = new DesktopIcon(icon, this.in_folder, this);
|
||||
let icon_html = icon_obj.get_desktop_icon_html();
|
||||
this.icons.push(icon_obj);
|
||||
this.icons_html.push(icon_html);
|
||||
this.setup_actions_on_icon(icon_obj);
|
||||
grid.append(icon_html);
|
||||
});
|
||||
this.setup_tooltip();
|
||||
}
|
||||
setup_actions_on_icon(icon) {
|
||||
if (this.edit_mode) {
|
||||
icon.edit_mode = true;
|
||||
}
|
||||
if (this.is_pane) {
|
||||
icon.in_pane = true;
|
||||
}
|
||||
}
|
||||
setup_tooltip() {
|
||||
$('[data-toggle="tooltip"]').tooltip({
|
||||
placement: "bottom",
|
||||
});
|
||||
}
|
||||
remove_label_tooltip() {
|
||||
$('[data-toggle="tooltip"]').tooltip("disable");
|
||||
}
|
||||
setup_reordering(grid) {
|
||||
const me = this;
|
||||
this.hoverTarget = null;
|
||||
|
|
@ -630,10 +718,22 @@ class DesktopIconGrid {
|
|||
sort: true, // keep sorting normally
|
||||
dragoverBubble: true,
|
||||
group: {
|
||||
name: "desktop",
|
||||
name: this.name || "desktop",
|
||||
put: true,
|
||||
pull: true,
|
||||
},
|
||||
onAdd(evt) {
|
||||
if (Sortable.get(evt.from).option("group").name == "hidden-icons-grid") {
|
||||
let icon_name = $(evt.item).attr("data-id");
|
||||
let icon = get_desktop_icon_by_label(icon_name, {}, true);
|
||||
icon.index = evt.newIndex;
|
||||
icon.hidden = 0;
|
||||
frappe.new_desktop_icons.push(icon);
|
||||
let hidden_icons = frappe.pages.desktop.desktop_page.hidden_icons;
|
||||
let added_icon_index = hidden_icons.findIndex((d) => d.label == icon_name);
|
||||
hidden_icons.splice(added_icon_index, 1);
|
||||
}
|
||||
},
|
||||
onStart(evt) {
|
||||
frappe.desktop_utils.dragged_item = evt.item;
|
||||
},
|
||||
|
|
@ -644,9 +744,6 @@ class DesktopIconGrid {
|
|||
});
|
||||
dataTransfer.setData("text/plain", JSON.stringify(icon.icon_data)); // `dataTransfer` object of HTML5 DragEvent
|
||||
},
|
||||
onMove() {
|
||||
return frappe.desktop_utils.allow_move || false;
|
||||
},
|
||||
onEnd: function (evt) {
|
||||
if (frappe.desktop_utils.in_folder_creation) return;
|
||||
if (evt.oldIndex !== evt.newIndex) {
|
||||
|
|
@ -679,6 +776,10 @@ class DesktopIconGrid {
|
|||
});
|
||||
}
|
||||
}
|
||||
update_grid(icons) {
|
||||
this.wrapper.empty();
|
||||
this.init();
|
||||
}
|
||||
reorder_icons(reordered_icons, filters) {
|
||||
reordered_icons.forEach((d, idx) => {
|
||||
let icon = get_desktop_icon_by_label(d);
|
||||
|
|
@ -694,7 +795,7 @@ class DesktopIconGrid {
|
|||
}
|
||||
}
|
||||
class DesktopIcon {
|
||||
constructor(icon, in_folder) {
|
||||
constructor(icon, in_folder, grid_obj) {
|
||||
this.icon_data = icon;
|
||||
this.icon_title = this.icon_data.label;
|
||||
this.icon_subtitle = "";
|
||||
|
|
@ -702,6 +803,8 @@ class DesktopIcon {
|
|||
this.in_folder = in_folder;
|
||||
this.icon_data.in_folder = in_folder;
|
||||
this.link_type = this.icon_data.link_type;
|
||||
this._edit_mode = false;
|
||||
this.in_pane = false;
|
||||
if (this.icon_type != "Folder" && !this.icon_data.sidebar) {
|
||||
this.icon_route = get_route(this.icon_data);
|
||||
}
|
||||
|
|
@ -720,12 +823,65 @@ class DesktopIcon {
|
|||
this.parent_icon = this.icon_data.icon;
|
||||
this.setup_click();
|
||||
this.render_folder_thumbnail();
|
||||
this.grid = grid_obj;
|
||||
Object.defineProperty(this, "edit_mode", {
|
||||
get: function () {
|
||||
return this._edit_mode;
|
||||
},
|
||||
set: function (value) {
|
||||
if (value) {
|
||||
this.icon.addClass("desktop-edit-mode");
|
||||
if (this.in_folder) {
|
||||
this.icon.removeClass("desktop-edit-mode");
|
||||
}
|
||||
this.grid.remove_label_tooltip();
|
||||
this.setup_dragging();
|
||||
this.setup_edit_menu();
|
||||
this.setup_hide_button();
|
||||
this.icon.removeAttr("href");
|
||||
} else {
|
||||
this.icon.addClass("desktop-edit-mode");
|
||||
this.setup_click();
|
||||
}
|
||||
this._edit_mode = value;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(this, "in_pane", {
|
||||
get: function () {
|
||||
return this._in_pane;
|
||||
},
|
||||
set: function (value) {
|
||||
this._in_pane = value;
|
||||
if (value) {
|
||||
this.icon.find(".hide-button").html(frappe.utils.icon("plus"));
|
||||
this.icon.find(".hide-button").attr("data-mode", "add");
|
||||
this.setup_hide_button();
|
||||
} else {
|
||||
this.icon.find(".hide-button").html(frappe.utils.icon("x"));
|
||||
this.icon.find(".hide-button").attr("data-mode", "hide");
|
||||
}
|
||||
},
|
||||
});
|
||||
frappe.desktop_icons_objects.push(this);
|
||||
}
|
||||
|
||||
// this.child_icons = this.get_desktop_icon(this.icon_title).child_icons;
|
||||
// this.child_icons_data = this.get_child_icons_data();
|
||||
}
|
||||
setup_hide_button() {
|
||||
this.icon.find(".hide-button").on("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
let desktop_label = event.currentTarget.parentElement.dataset.id;
|
||||
let desktop_icon = get_desktop_icon_by_label(desktop_label);
|
||||
if (event.target.parentElement.dataset.mode == "hide") {
|
||||
desktop_icon.hidden = 1;
|
||||
} else {
|
||||
desktop_icon.hidden = 0;
|
||||
}
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
});
|
||||
}
|
||||
validate_icon() {
|
||||
// validate if my workspaces are empty
|
||||
if (this.icon_data.label == "My Workspaces") {
|
||||
|
|
@ -735,9 +891,6 @@ class DesktopIcon {
|
|||
if (this.icon_type == "Folder") {
|
||||
if (this.icon_data.child_icons.length == 0) return false;
|
||||
}
|
||||
if (this.icon_type == "Link" && !this.icon_route) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
get_child_icons_data() {
|
||||
|
|
@ -746,36 +899,114 @@ class DesktopIcon {
|
|||
get_desktop_icon_html() {
|
||||
return this.icon;
|
||||
}
|
||||
setup_edit_menu() {
|
||||
const me = frappe.pages["desktop"].desktop_page;
|
||||
let icon_data = this.icon_data;
|
||||
const icon = this;
|
||||
frappe.ui.create_menu({
|
||||
parent: this.icon,
|
||||
right_click: true,
|
||||
menu_items: [
|
||||
{
|
||||
label: "Edit",
|
||||
icon: "edit",
|
||||
condition: function () {
|
||||
return icon_data.standard != 1;
|
||||
},
|
||||
onClick: function () {
|
||||
frappe.ui.form.make_quick_entry(
|
||||
"Desktop Icon",
|
||||
function (icon) {
|
||||
let old_index = frappe.new_desktop_icons.findIndex(
|
||||
(d_icon) => d_icon.label == icon.label
|
||||
);
|
||||
if (old_index !== -1) {
|
||||
frappe.new_desktop_icons.splice(old_index, 1);
|
||||
}
|
||||
frappe.new_desktop_icons.push(icon);
|
||||
frappe.new_icons.push(icon.name);
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
},
|
||||
function (dialog) {
|
||||
dialog.set_df_property("label", "read_only", 1);
|
||||
dialog.fields.forEach((field) => {
|
||||
field.default = icon_data[field.fieldname];
|
||||
});
|
||||
dialog.script_manager.trigger("refresh");
|
||||
},
|
||||
icon_data,
|
||||
null
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Create Folder",
|
||||
icon: "folder",
|
||||
onClick: function () {
|
||||
let folder = me.grid.add_folder();
|
||||
add_icons_to_folder(folder.label, [icon_data.label]);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Add To Folder",
|
||||
icon: "folder-open",
|
||||
condition: function () {
|
||||
return me.folders.length > 0;
|
||||
},
|
||||
items: me.folders.map((name) => {
|
||||
return {
|
||||
label: name,
|
||||
onClick: function () {
|
||||
add_icons_to_folder(this.label, [icon_data.label]);
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
setup_click() {
|
||||
const me = this;
|
||||
if (this.child_icons?.length && (this.icon_type == "App" || this.icon_type == "Folder")) {
|
||||
$(this.icon).on("click", () => {
|
||||
let modal = frappe.desktop_utils.create_desktop_modal(me);
|
||||
modal.setup(me.icon_title, me.child_icons, 4);
|
||||
let $title = modal.modal.find(".modal-title");
|
||||
let title = new InlineEditor($title, this.icon_data.label, function (
|
||||
old_value,
|
||||
new_value
|
||||
) {
|
||||
let icon = get_desktop_icon_by_label(old_value);
|
||||
let folder_icons = frappe.desktop_utils.get_folder_icons(old_value);
|
||||
if (icon) {
|
||||
icon.label = new_value;
|
||||
}
|
||||
add_icons_to_folder(new_value, folder_icons);
|
||||
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
});
|
||||
modal.show();
|
||||
});
|
||||
if (this.icon_type == "App") {
|
||||
$($(this.icon_caption_area).children()[1]).html(
|
||||
`${this.child_icons.length} Workspaces`
|
||||
);
|
||||
let content = `${this.child_icons.length} Workspaces`;
|
||||
$($(this.icon_caption_area).children()[1]).html(__(content));
|
||||
}
|
||||
} else {
|
||||
this.icon.attr("href", this.icon_route);
|
||||
}
|
||||
if (this.icon_data.sidebar) {
|
||||
const me = this;
|
||||
this.icon.on("click", function () {
|
||||
if (me.icon_data.sidebar == "My Workspaces") {
|
||||
let sidebar_name = me.icon_data.sidebar.toLowerCase();
|
||||
if (frappe.boot.workspace_sidebar_item[sidebar_name].items.length == 0) {
|
||||
frappe.toast("No Private Workspaces for user");
|
||||
} else {
|
||||
let workspace_name =
|
||||
frappe.boot.workspace_sidebar_item[sidebar_name].items[0]["link_to"];
|
||||
frappe.set_route("Workspaces", "private", workspace_name);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this.icon_route && this.icon_route.startsWith("http")) {
|
||||
this.icon.attr("target", "_blank");
|
||||
}
|
||||
if (this.icon_route) {
|
||||
this.icon.attr("href", this.icon_route);
|
||||
} else {
|
||||
this.icon.on("click", function (event) {
|
||||
frappe.msgprint(
|
||||
__(
|
||||
"Icon is not correctly configured please check the workspace sidebar to it"
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -821,51 +1052,6 @@ class DesktopIcon {
|
|||
}
|
||||
}
|
||||
});
|
||||
this.icon.on("dragstart", function (event) {
|
||||
frappe.desktop_utils.dragged_item = event.target;
|
||||
});
|
||||
this.icon.on("dragover", function (event) {
|
||||
console.log(event.target);
|
||||
if (frappe.desktop_utils.dragged_item == event.target.parentElement) return;
|
||||
if (
|
||||
event.target == frappe.desktop_utils.dragged_item ||
|
||||
frappe.desktop_utils.dragged_item.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (event.target.parentElement.classList.contains("icon-container")) {
|
||||
frappe.desktop_utils.allow_move = false;
|
||||
frappe.desktop_utils.in_folder_creation = true;
|
||||
|
||||
let icon_list = [];
|
||||
icon_list.push(
|
||||
get_desktop_icon_by_label(event.target.parentElement.parentElement.dataset.id)
|
||||
);
|
||||
icon_list.push(
|
||||
get_desktop_icon_by_label(frappe.desktop_utils.dragged_item.dataset.id)
|
||||
);
|
||||
|
||||
let icon = {
|
||||
label: "Untitled Folder",
|
||||
icon_type: "Folder",
|
||||
child_icons: icon_list,
|
||||
};
|
||||
let modal = frappe.desktop_utils.create_desktop_modal(icon);
|
||||
modal.setup(icon.label, icon_list, 4);
|
||||
$(event.target.parentElement).addClass("folder-icon");
|
||||
$(event.target.parentElement).empty();
|
||||
modal.show();
|
||||
frappe.boot.desktop_icons.push(icon);
|
||||
icon_list.forEach((icon) => {
|
||||
let desktop_icon = frappe.utils.get_desktop_icon_by_label(icon.label);
|
||||
desktop_icon.parent_icon = "Untitled Folder";
|
||||
frappe.new_desktop_icons.splice(frappe.boot.desktop_icons.indexOf(icon), 1);
|
||||
frappe.new_desktop_icons.push(desktop_icon);
|
||||
});
|
||||
} else {
|
||||
frappe.desktop_utils.allow_move = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -876,6 +1062,10 @@ class DesktopModal {
|
|||
setup(icon_title, child_icons_data, grid_row_size) {
|
||||
const me = this;
|
||||
this.make_modal(icon_title);
|
||||
|
||||
// Check if we're in edit mode
|
||||
const is_edit_mode = frappe.pages["desktop"].desktop_page.edit_mode;
|
||||
|
||||
this.child_icon_grid = new DesktopIconGrid({
|
||||
wrapper: this.$child_icons_wrapper,
|
||||
icons_data: child_icons_data,
|
||||
|
|
@ -883,8 +1073,16 @@ class DesktopModal {
|
|||
in_folder: false,
|
||||
in_modal: true,
|
||||
parent_icon: this.parent_icon_obj,
|
||||
edit_mode: is_edit_mode, // Pass edit mode state
|
||||
});
|
||||
|
||||
// If in edit mode, setup reordering for the modal icons
|
||||
if (is_edit_mode) {
|
||||
this.child_icon_grid.grids.forEach((grid) => {
|
||||
this.child_icon_grid.setup_reordering(grid);
|
||||
});
|
||||
}
|
||||
|
||||
this.modal.on("hidden.bs.modal", function () {
|
||||
me.modal.remove();
|
||||
frappe.desktop_utils.modal = null;
|
||||
|
|
@ -934,3 +1132,93 @@ class DesktopModal {
|
|||
this.modal.modal("hide");
|
||||
}
|
||||
}
|
||||
|
||||
class IconsPane {
|
||||
constructor() {
|
||||
this.wrapper = $($(".desktop-container .icons-container").get(0));
|
||||
}
|
||||
show() {
|
||||
this.wrapper.removeClass("hidden");
|
||||
if (this.grid) {
|
||||
this.grid.icons_data = frappe.pages.desktop.desktop_page.hidden_icons;
|
||||
this.grid.update_grid();
|
||||
return;
|
||||
}
|
||||
this.wrapper.append(
|
||||
"<span style='margin-top: 10px; margin-bottom: 20px'>Removed Icons</span>"
|
||||
);
|
||||
this.grid = new DesktopIconGrid({
|
||||
name: "hidden-icons-grid",
|
||||
wrapper: this.wrapper,
|
||||
icons_data: frappe.pages.desktop.desktop_page.hidden_icons,
|
||||
row_size: 6,
|
||||
edit_mode: true,
|
||||
compact: true,
|
||||
is_pane: true,
|
||||
});
|
||||
this.setup();
|
||||
}
|
||||
hide() {
|
||||
this.wrapper.addClass("hidden");
|
||||
}
|
||||
setup() {
|
||||
this.setup_close_button();
|
||||
}
|
||||
setup_close_button() {
|
||||
const me = this;
|
||||
this.wrapper.find(".close-button").on("click", function () {
|
||||
me.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class InlineEditor {
|
||||
constructor(container, initialValue = "", onRename = () => {}) {
|
||||
this.container = container;
|
||||
this.initialValue = initialValue;
|
||||
this.onRename = onRename;
|
||||
|
||||
this.render();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.html(`
|
||||
<div class="title-widget">
|
||||
<div class="title-input-label">
|
||||
<span>${__(this.initialValue)}</span>
|
||||
</div>
|
||||
<div class="title-input-wrapper">
|
||||
<input class="title-input">
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.input = this.container.find(".title-input");
|
||||
this.label = this.container.find(".title-input-label");
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.container.on("click", () => {
|
||||
if (frappe.pages["desktop"].desktop_page.edit_mode) {
|
||||
this.label.css("visibility", "hidden");
|
||||
this.input.focus().select();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.on("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
const newValue = this.input.val().trim();
|
||||
this.input.css("display", "none");
|
||||
this.label.css("visibility", "visible");
|
||||
this.label.find("span").text(newValue);
|
||||
|
||||
this.onRename(this.initialValue, newValue, this);
|
||||
}
|
||||
});
|
||||
|
||||
this.input.on("blur", () => {
|
||||
this.label.css("visibility", "visible");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ def get_context(context):
|
|||
if not brand_logo:
|
||||
brand_logo = frappe.get_hooks("app_logo_url", app_name="frappe")[0]
|
||||
context.brand_logo = brand_logo
|
||||
context.desktop_icons = get_desktop_icons()
|
||||
context.current_user = frappe.session.user
|
||||
# check if system is mac or not
|
||||
context.is_mac = sys.platform == "darwin"
|
||||
try:
|
||||
context.desktop_layout = frappe.get_doc("Desktop Layout", frappe.session.user).layout or {}
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_last_message()
|
||||
context.desktop_layout = {}
|
||||
return context
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ def _export_query(form_params, csv_params, populate_response=True):
|
|||
|
||||
if file_format_type == "CSV":
|
||||
content = get_csv_bytes(
|
||||
[[handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r] for r in xlsx_data],
|
||||
[[handle_html(v) if isinstance(v, str) else v for v in r] for r in xlsx_data],
|
||||
csv_params,
|
||||
)
|
||||
file_extension = "csv"
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ def _export_query(form_params, csv_params, populate_response=True):
|
|||
if file_format_type == "CSV":
|
||||
file_extension = "csv"
|
||||
content = get_csv_bytes(
|
||||
[[handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r] for r in data],
|
||||
[[handle_html(v) if isinstance(v, str) else v for v in r] for r in data],
|
||||
csv_params,
|
||||
)
|
||||
elif file_format_type == "Excel":
|
||||
|
|
|
|||
|
|
@ -109,31 +109,6 @@ def search_widget(
|
|||
if filters is None:
|
||||
filters = {}
|
||||
|
||||
are_filters_dict = isinstance(filters, dict)
|
||||
include_disabled = False
|
||||
if not query and are_filters_dict:
|
||||
if "include_disabled" in filters:
|
||||
if filters["include_disabled"] == 1:
|
||||
include_disabled = True
|
||||
filters.pop("include_disabled")
|
||||
|
||||
filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()]
|
||||
are_filters_dict = False
|
||||
|
||||
if for_link_validation:
|
||||
if are_filters_dict:
|
||||
# we add filter if possible, otherwise rely on txt
|
||||
if "name" not in filters:
|
||||
filters["name"] = txt
|
||||
else:
|
||||
filters.append([doctype, "name", "=", txt])
|
||||
|
||||
as_dict = False
|
||||
# for custom queries that don't respect filters but respect limit (rare)
|
||||
# or for when we have to rely on txt
|
||||
# we want to match "A" with "A" only and not "A1", "BA" etc.
|
||||
page_length = PAGE_LENGTH_FOR_LINK_VALIDATION
|
||||
|
||||
if query: # Query = custom search query i.e. python function
|
||||
try:
|
||||
is_whitelisted(frappe.get_attr(query))
|
||||
|
|
@ -163,6 +138,19 @@ def search_widget(
|
|||
return []
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
include_disabled = False
|
||||
if isinstance(filters, dict):
|
||||
if "include_disabled" in filters:
|
||||
if filters["include_disabled"] == 1:
|
||||
include_disabled = True
|
||||
filters.pop("include_disabled")
|
||||
|
||||
filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()]
|
||||
|
||||
if for_link_validation:
|
||||
filters.append([doctype, "name", "=", txt])
|
||||
|
||||
or_filters = []
|
||||
|
||||
# build from doctype
|
||||
|
|
@ -364,8 +352,8 @@ def build_for_autosuggest(res: list[tuple], doctype: str) -> list[LinkSearchResu
|
|||
results.append(autosuggest_row)
|
||||
else:
|
||||
for item in res:
|
||||
value = _(item[0]) if meta.translated_doctype else item[0]
|
||||
results.append({"value": item[0], "description": to_string(item[1:]), "label": value})
|
||||
label = _(item[0]) if meta.translated_doctype else item[0]
|
||||
results.append({"value": item[0], "description": to_string(item[1:]), "label": label})
|
||||
|
||||
return results
|
||||
|
||||
|
|
@ -405,7 +393,7 @@ def get_names_for_mentions(search_term):
|
|||
def get_users_for_mentions():
|
||||
return frappe.get_all(
|
||||
"User",
|
||||
fields=["name as id", "full_name as value"],
|
||||
fields=["name as id", "full_name as value", "email"],
|
||||
filters={
|
||||
"name": ["not in", ("Administrator", "Guest")],
|
||||
"allowed_in_mentions": True,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class TestEmailAccount(IntegrationTestCase):
|
|||
TestEmailAccount.mocked_email_receive(email_account, messages)
|
||||
|
||||
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
|
||||
self.assertTrue("From: "Microsoft Outlook" <test_sender@example.com>" in comm.content)
|
||||
self.assertTrue('From: "Microsoft Outlook" <test_sender@example.com>' in comm.content)
|
||||
self.assertTrue(
|
||||
"This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content
|
||||
)
|
||||
|
|
@ -153,7 +153,7 @@ class TestEmailAccount(IntegrationTestCase):
|
|||
TestEmailAccount.mocked_email_receive(email_account, messages)
|
||||
|
||||
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
|
||||
self.assertTrue("From: "Microsoft Outlook" <test_sender@example.com>" in comm.content)
|
||||
self.assertTrue('From: "Microsoft Outlook" <test_sender@example.com>' in comm.content)
|
||||
self.assertTrue(
|
||||
"This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ app_publisher = "Frappe Technologies"
|
|||
app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node"
|
||||
app_license = "MIT"
|
||||
app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg"
|
||||
develop_version = "15.x.x-develop"
|
||||
develop_version = "17.x.x-develop"
|
||||
app_home = "/app/build"
|
||||
|
||||
app_email = "developers@frappe.io"
|
||||
|
|
|
|||
8865
frappe/locale/ar.po
8865
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
3065
frappe/locale/bs.po
3065
frappe/locale/bs.po
File diff suppressed because it is too large
Load diff
3054
frappe/locale/cs.po
3054
frappe/locale/cs.po
File diff suppressed because it is too large
Load diff
3054
frappe/locale/da.po
3054
frappe/locale/da.po
File diff suppressed because it is too large
Load diff
3065
frappe/locale/de.po
3065
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
3064
frappe/locale/eo.po
3064
frappe/locale/eo.po
File diff suppressed because it is too large
Load diff
5668
frappe/locale/es.po
5668
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
5340
frappe/locale/fa.po
5340
frappe/locale/fa.po
File diff suppressed because it is too large
Load diff
5410
frappe/locale/fr.po
5410
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
3071
frappe/locale/hr.po
3071
frappe/locale/hr.po
File diff suppressed because it is too large
Load diff
3133
frappe/locale/hu.po
3133
frappe/locale/hu.po
File diff suppressed because it is too large
Load diff
3143
frappe/locale/id.po
3143
frappe/locale/id.po
File diff suppressed because it is too large
Load diff
3225
frappe/locale/it.po
3225
frappe/locale/it.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3054
frappe/locale/my.po
3054
frappe/locale/my.po
File diff suppressed because it is too large
Load diff
3065
frappe/locale/nb.po
3065
frappe/locale/nb.po
File diff suppressed because it is too large
Load diff
3202
frappe/locale/nl.po
3202
frappe/locale/nl.po
File diff suppressed because it is too large
Load diff
5073
frappe/locale/pl.po
5073
frappe/locale/pl.po
File diff suppressed because it is too large
Load diff
3062
frappe/locale/pt.po
3062
frappe/locale/pt.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
5647
frappe/locale/ru.po
5647
frappe/locale/ru.po
File diff suppressed because it is too large
Load diff
3056
frappe/locale/sl.po
3056
frappe/locale/sl.po
File diff suppressed because it is too large
Load diff
3065
frappe/locale/sr.po
3065
frappe/locale/sr.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3078
frappe/locale/sv.po
3078
frappe/locale/sv.po
File diff suppressed because it is too large
Load diff
3058
frappe/locale/th.po
3058
frappe/locale/th.po
File diff suppressed because it is too large
Load diff
4959
frappe/locale/tr.po
4959
frappe/locale/tr.po
File diff suppressed because it is too large
Load diff
3058
frappe/locale/vi.po
3058
frappe/locale/vi.po
File diff suppressed because it is too large
Load diff
3064
frappe/locale/zh.po
3064
frappe/locale/zh.po
File diff suppressed because it is too large
Load diff
|
|
@ -1404,7 +1404,10 @@ class BaseDocument:
|
|||
):
|
||||
currency = frappe.db.get_value("Currency", currency_value, cache=True)
|
||||
|
||||
val = self.get(fieldname)
|
||||
if fieldname and (prop := getattr(type(self), fieldname, None)) and is_a_property(prop):
|
||||
val = getattr(self, fieldname)
|
||||
else:
|
||||
val = self.get(fieldname)
|
||||
|
||||
if translated:
|
||||
val = _(val)
|
||||
|
|
|
|||
|
|
@ -240,9 +240,6 @@ def map_fetch_fields(target_doc, df, no_copy_fields):
|
|||
|
||||
# options should be like "link_fieldname.fieldname_in_liked_doc"
|
||||
for fetch_df in target_doc.meta.get("fields", {"fetch_from": f"^{df.fieldname}."}):
|
||||
if not (fetch_df.fieldtype == "Read Only" or fetch_df.read_only):
|
||||
continue
|
||||
|
||||
if (
|
||||
not target_doc.get(fetch_df.fieldname) or fetch_df.fieldtype == "Read Only"
|
||||
) and fetch_df.fieldname not in no_copy_fields:
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ import datetime
|
|||
import re
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID
|
||||
|
||||
import uuid_utils
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid7
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -166,8 +164,8 @@ def set_new_name(doc):
|
|||
|
||||
if meta.autoname == "UUID":
|
||||
if not doc.name:
|
||||
doc.name = str(uuid_utils.uuid7())
|
||||
elif isinstance(doc.name, UUID | uuid_utils.UUID):
|
||||
doc.name = str(uuid7())
|
||||
elif isinstance(doc.name, UUID):
|
||||
doc.name = str(doc.name)
|
||||
elif isinstance(doc.name, str): # validate
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -201,13 +201,14 @@ def remove_orphan_doctypes():
|
|||
|
||||
def remove_orphan_entities():
|
||||
entites = ["Workspace", "Dashboard", "Page", "Report"]
|
||||
app_level_entities = ["Workspace Sidebar"]
|
||||
app_level_entities = ["Workspace Sidebar", "Desktop Icon"]
|
||||
entity_filter_map = {
|
||||
"Workspace": {"public": 1},
|
||||
"Page": {"standard": "Yes"},
|
||||
"Report": {"is_standard": "Yes"},
|
||||
"Dashboard": {"is_standard": True},
|
||||
"Workspace Sidebar": {"standard": True},
|
||||
"Desktop Icon": {"standard": True},
|
||||
}
|
||||
entity_file_map = create_entity_file_map(entites)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ def update_user_settings(doctype, user_settings, for_update=False):
|
|||
current = {}
|
||||
|
||||
current.update(user_settings)
|
||||
|
||||
frappe.cache.hset("_user_settings", f"{doctype}::{frappe.session.user}", json.dumps(current))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import re
|
|||
from http import cookies
|
||||
from urllib.parse import unquote, urljoin, urlparse
|
||||
|
||||
import jwt
|
||||
from oauthlib.openid import RequestValidator
|
||||
|
||||
import frappe
|
||||
|
|
@ -302,6 +301,8 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# OpenID Connect
|
||||
|
||||
def finalize_id_token(self, id_token, token, token_handler, request):
|
||||
import jwt
|
||||
|
||||
# Check whether frappe server URL is set
|
||||
id_token_header = {"typ": "jwt", "alg": "HS256"}
|
||||
|
||||
|
|
@ -437,6 +438,8 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
"""
|
||||
import jwt
|
||||
|
||||
if id_token_hint:
|
||||
try:
|
||||
user = None
|
||||
|
|
|
|||
|
|
@ -239,7 +239,6 @@ frappe.patches.v15_0.migrate_session_data
|
|||
frappe.custom.doctype.property_setter.patches.remove_invalid_fetch_from_expressions
|
||||
frappe.patches.v16_0.switch_default_sort_order
|
||||
frappe.integrations.doctype.oauth_client.patches.set_default_allowed_role_in_oauth_client
|
||||
execute:frappe.db.set_single_value("Workspace Settings", "workspace_setup_completed", 1)
|
||||
frappe.patches.v16_0.add_app_launcher_in_navbar_settings
|
||||
frappe.desk.doctype.workspace.patches.update_app
|
||||
frappe.patches.v16_0.move_role_desk_settings_to_user
|
||||
|
|
@ -256,3 +255,4 @@ frappe.patches.v16_0.change_link_type_to_workspace_sidebar
|
|||
frappe.patches.v16_0.add_standard_field_in_workspace_sidebar
|
||||
execute:frappe.db.set_single_value("Desktop Settings", "icon_style", "Solid")
|
||||
execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Productivity")
|
||||
frappe.patches.v16_0.unset_standard_field_for_auto_generated_icons
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import frappe
|
||||
from frappe.model.sync import check_if_record_exists
|
||||
|
||||
|
||||
def execute():
|
||||
for icon in frappe.get_all("Desktop Icon"):
|
||||
icon_doc = frappe.get_doc("Desktop Icon", icon.name)
|
||||
if (icon_doc.standard and icon_doc.app) and not check_if_record_exists(
|
||||
"app",
|
||||
frappe.get_app_path(icon_doc.app),
|
||||
"Desktop Icon",
|
||||
icon_doc.name,
|
||||
):
|
||||
try:
|
||||
icon_doc.standard = 0
|
||||
icon_doc.save()
|
||||
except Exception as e:
|
||||
print("Error in unsetting standard field", e)
|
||||
|
|
@ -704,6 +704,40 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
update_column_count_message();
|
||||
});
|
||||
|
||||
// Toggle all checkboxes in column selector
|
||||
const toggle_all_checkboxes = function (should_check, should_clear_value) {
|
||||
// Scope to column selector list checkboxes only
|
||||
$body
|
||||
.find(".column-selector-list input[type='checkbox'][data-fieldname]")
|
||||
.each(function () {
|
||||
const $checkbox = $(this);
|
||||
const is_checked = $checkbox.prop("checked");
|
||||
|
||||
// Only process checkboxes that need to be changed
|
||||
if ((should_check && !is_checked) || (!should_check && is_checked)) {
|
||||
$checkbox.prop("checked", should_check);
|
||||
const fieldname = $checkbox.attr("data-fieldname");
|
||||
const input = get_width_input(fieldname);
|
||||
input.prop("disabled", !should_check);
|
||||
|
||||
if (should_clear_value) {
|
||||
input.val("");
|
||||
}
|
||||
}
|
||||
});
|
||||
update_column_count_message();
|
||||
};
|
||||
|
||||
// Select All functionality
|
||||
$body.on("click", ".select-all-btn", function () {
|
||||
toggle_all_checkboxes(true, false);
|
||||
});
|
||||
|
||||
// Unselect All functionality
|
||||
$body.on("click", ".unselect-all-btn", function () {
|
||||
toggle_all_checkboxes(false, true);
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
<p class="help-message alert alert-warning">
|
||||
{{ __("Some columns might get cut off when printing to PDF. Try to keep number of columns under 10.") }}
|
||||
</p>
|
||||
<div class="row" style="margin-bottom: 15px;">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-xs btn-default select-all-btn" type="button">{{ __("Select All") }}</button>
|
||||
<button class="btn btn-xs btn-default unselect-all-btn" type="button" style="margin-left: 5px;">{{ __("Unselect All") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6"><p class="bold">{{ __("Column") }}</p></div>
|
||||
<div class="col-sm-6 text-right"><p class="bold">{{ __("Width") }}</p></div>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ input {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label .checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -636,10 +636,13 @@ function upload_file(file, i) {
|
|||
: __("File upload failed.");
|
||||
} else {
|
||||
file.failed = true;
|
||||
let detail =
|
||||
xhr.statusText ||
|
||||
__("Server error during upload. The file might be corrupted.");
|
||||
file.error_message =
|
||||
xhr.status === 0
|
||||
? __("XMLHttpRequest Error")
|
||||
: `${xhr.status} : ${xhr.statusText}`;
|
||||
: `${xhr.status} : ${detail}`;
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,18 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
|
|||
// like links, currencies, HTMLs etc.
|
||||
this.disp_area = this.$wrapper.find(".control-value").get(0);
|
||||
}
|
||||
this.setup_shortcut();
|
||||
}
|
||||
setup_shortcut() {
|
||||
$(this.input_area).on("keydown", function (event) {
|
||||
if (event.originalEvent.ctrlKey || event.originalEvent.metaKey) {
|
||||
if (event.originalEvent.key === "k" || event.originalEvent.key === "K") {
|
||||
$("#navbar-modal-search").click();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
set_max_width() {
|
||||
if (this.constructor.horizontal) {
|
||||
|
|
@ -165,7 +177,16 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
|
|||
let doc = this.doc || (this.frm && this.frm.doc);
|
||||
let display_value = frappe.format(value, this.df, { no_icon: true, inline: true }, doc);
|
||||
// This is used to display formatted output AND showing values in read only fields
|
||||
this.disp_area && $(this.disp_area).html(display_value);
|
||||
if (this.disp_area) {
|
||||
$(this.disp_area).html(display_value);
|
||||
// Apply alignment only for supported fields
|
||||
if (
|
||||
this.df.alignment &&
|
||||
["Data", "Int", "Float", "Currency", "Percent"].includes(this.df.fieldtype)
|
||||
) {
|
||||
$(this.disp_area).css("text-align", this.df.alignment.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
set_label(label) {
|
||||
if (label) this.df.label = label;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue