Merge remote-tracking branch 'upstream/develop' into list-padding
This commit is contained in:
commit
75f588d386
39 changed files with 588 additions and 430 deletions
|
|
@ -85,7 +85,7 @@ context("Dynamic Link", () => {
|
|||
//Checking if the listbox have length greater than 0
|
||||
cy.get('[data-fieldname="doc_id"]')
|
||||
.find(".awesomplete")
|
||||
.find("li")
|
||||
.find("div")
|
||||
.its("length")
|
||||
.should("be.gte", 0);
|
||||
cy.get(".btn-modal-close").click({ force: true });
|
||||
|
|
@ -100,7 +100,7 @@ context("Dynamic Link", () => {
|
|||
//Checking if the listbox have length greater than 0
|
||||
cy.get('[data-fieldname="doc_id"]')
|
||||
.find(".awesomplete")
|
||||
.find("li")
|
||||
.find("div")
|
||||
.its("length")
|
||||
.should("be.gte", 0);
|
||||
cy.get(".btn-modal-close").click({ force: true, multiple: true });
|
||||
|
|
@ -119,7 +119,7 @@ context("Dynamic Link", () => {
|
|||
//Checking if the listbox have length greater than 0
|
||||
cy.get('[data-fieldname="doc_id"]')
|
||||
.find(".awesomplete")
|
||||
.find("li")
|
||||
.find("div")
|
||||
.its("length")
|
||||
.should("be.gte", 0);
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ context("Dynamic Link", () => {
|
|||
//Checking if the listbox have length greater than 0
|
||||
cy.get('[data-fieldname="doc_id"]')
|
||||
.find(".awesomplete")
|
||||
.find("li")
|
||||
.find("div")
|
||||
.its("length")
|
||||
.should("be.gte", 0);
|
||||
cy.get_field("doc_type").clear();
|
||||
|
|
@ -143,7 +143,7 @@ context("Dynamic Link", () => {
|
|||
cy.intercept("/api/method/frappe.desk.search.search_link").as("search_query");
|
||||
cy.fill_field("doc_type", "System Settings", "Link", { delay: 500 });
|
||||
cy.wait("@search_query");
|
||||
cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`).click({
|
||||
cy.get(`[data-fieldname="doc_type"] ul:visible div:first-child`).click({
|
||||
scrollBehavior: false,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@
|
|||
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
|
|
@ -236,6 +237,7 @@
|
|||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Small Text",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Default",
|
||||
"max_height": "3rem",
|
||||
"oldfieldname": "default",
|
||||
|
|
@ -582,7 +584,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:02:18.210626",
|
||||
"modified": "2024-04-12 16:27:34.546314",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
@ -592,4 +594,4 @@
|
|||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
"desk_settings_section",
|
||||
"mute_sounds",
|
||||
"desk_theme",
|
||||
"code_editor_type",
|
||||
"banner_image",
|
||||
"change_password",
|
||||
"new_password",
|
||||
|
|
@ -726,6 +727,13 @@
|
|||
"fieldname": "workspace_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Workspace"
|
||||
},
|
||||
{
|
||||
"default": "vscode",
|
||||
"fieldname": "code_editor_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Code Editor Type",
|
||||
"options": "vscode\nvim\nemacs"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
|
|
@ -788,7 +796,7 @@
|
|||
"link_fieldname": "user"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-23 16:03:59.833565",
|
||||
"modified": "2024-04-12 23:25:04.628007",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
@ -827,4 +835,4 @@
|
|||
"states": [],
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ class User(Document):
|
|||
birth_date: DF.Date | None
|
||||
block_modules: DF.Table[BlockModule]
|
||||
bypass_restrict_ip_check_if_2fa_enabled: DF.Check
|
||||
code_editor_type: DF.Literal["vscode", "vim", "emacs"]
|
||||
default_workspace: DF.Link | None
|
||||
defaults: DF.Table[DefaultValue]
|
||||
desk_theme: DF.Literal["Light", "Dark", "Automatic"]
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@
|
|||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
|
|
@ -186,6 +187,7 @@
|
|||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
|
|
@ -464,7 +466,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:02:15.143117",
|
||||
"modified": "2024-04-12 16:25:50.349736",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
@ -498,4 +500,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,14 @@ frappe.ui.form.on("Customize Form", {
|
|||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Trim Table"),
|
||||
function () {
|
||||
frm.trigger("trim_table");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
const is_autoname_autoincrement = frm.doc.autoname === "autoincrement";
|
||||
frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
|
||||
frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
|
||||
|
|
@ -194,6 +202,40 @@ frappe.ui.form.on("Customize Form", {
|
|||
);
|
||||
},
|
||||
|
||||
async trim_table(frm) {
|
||||
let dropped_columns = await frappe.xcall(
|
||||
"frappe.custom.doctype.customize_form.customize_form.get_orphaned_columns",
|
||||
{ doctype: frm.doc.doc_type }
|
||||
);
|
||||
|
||||
if (!dropped_columns?.length) {
|
||||
frappe.toast(__("This doctype has no orphan fields to trim"));
|
||||
return;
|
||||
}
|
||||
let msg = __(
|
||||
"Warning: DATA LOSS IMMINENT! Proceeding will permanently delete following database columns from doctype {0}:",
|
||||
[frm.doc.doc_type.bold()]
|
||||
);
|
||||
msg += "<ol>" + dropped_columns.map((col) => `<li>${col}</li>`).join("") + "</ol>";
|
||||
msg += __("This action is irreversible. Do you wish to continue?");
|
||||
|
||||
frappe.confirm(msg, () => {
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
method: "trim_table",
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert({
|
||||
message: __("Table Trimmed"),
|
||||
indicator: "green",
|
||||
});
|
||||
frappe.customize_form.clear_locals_and_refresh(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setup_export(frm) {
|
||||
if (frappe.boot.developer_mode) {
|
||||
frm.add_custom_button(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from frappe.custom.doctype.property_setter.property_setter import delete_propert
|
|||
from frappe.model import core_doctypes_list, no_value_fields
|
||||
from frappe.model.docfield import supports_translation
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import trim_table
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
|
|
@ -639,6 +640,19 @@ class CustomizeForm(Document):
|
|||
frappe.clear_cache(doctype=self.doc_type)
|
||||
self.fetch_to_customize()
|
||||
|
||||
@frappe.whitelist()
|
||||
def trim_table(self):
|
||||
"""Removes database fields that don't exist in the doctype.
|
||||
|
||||
This may be needed as maintenance since removing a field in a DocType
|
||||
doesn't automatically delete the db field.
|
||||
"""
|
||||
if not self.doc_type:
|
||||
return
|
||||
|
||||
trim_table(self.doc_type, dry_run=False)
|
||||
self.fetch_to_customize()
|
||||
|
||||
@classmethod
|
||||
def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
|
||||
"""allow type change, if both old_type and new_type are in same field group.
|
||||
|
|
@ -651,6 +665,13 @@ class CustomizeForm(Document):
|
|||
return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_orphaned_columns(doctype: str):
|
||||
frappe.only_for("System Manager")
|
||||
frappe.db.begin(read_only=True) # Avoid any potential bug from writing to db
|
||||
return trim_table(doctype, dry_run=True)
|
||||
|
||||
|
||||
def reset_customization(doctype):
|
||||
setters = frappe.get_all(
|
||||
"Property Setter",
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class EmailServer:
|
|||
if cint(self.settings.use_imap):
|
||||
self.check_imap_uidvalidity(folder)
|
||||
|
||||
readonly = False if self.settings.email_sync_rule == "UNSEEN" else True
|
||||
readonly = self.settings.email_sync_rule != "UNSEEN"
|
||||
|
||||
self.imap.select(folder, readonly=readonly)
|
||||
response, message = self.imap.uid("search", None, self.settings.email_sync_rule)
|
||||
|
|
@ -213,27 +213,32 @@ class EmailServer:
|
|||
|
||||
if not uid_validity or uid_validity != current_uid_validity:
|
||||
# uidvalidity changed & all email uids are reindexed by server
|
||||
Communication = frappe.qb.DocType("Communication")
|
||||
frappe.qb.update(Communication).set(Communication.uid, -1).where(
|
||||
Communication.communication_medium == "Email"
|
||||
).where(Communication.email_account == self.settings.email_account).run()
|
||||
frappe.db.set_value(
|
||||
"Communication",
|
||||
{"communication_medium": "Email", "email_account": self.settings.email_account},
|
||||
"uid",
|
||||
-1,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
if self.settings.use_imap:
|
||||
# Remove {"} quotes that are added to handle spaces in IMAP Folder names
|
||||
if folder[0] == folder[-1] == '"':
|
||||
folder = folder[1:-1]
|
||||
# new update for the IMAP Folder DocType
|
||||
IMAPFolder = frappe.qb.DocType("IMAP Folder")
|
||||
frappe.qb.update(IMAPFolder).set(IMAPFolder.uidvalidity, current_uid_validity).set(
|
||||
IMAPFolder.uidnext, uidnext
|
||||
).where(IMAPFolder.parent == self.settings.email_account_name).where(
|
||||
IMAPFolder.folder_name == folder
|
||||
).run()
|
||||
|
||||
frappe.db.set_value(
|
||||
"IMAP Folder",
|
||||
{"parent": self.settings.email_account_name, "folder_name": folder},
|
||||
{"uidvalidity": current_uid_validity, "uidnext": uidnext},
|
||||
update_modified=False,
|
||||
)
|
||||
else:
|
||||
EmailAccount = frappe.qb.DocType("Email Account")
|
||||
frappe.qb.update(EmailAccount).set(EmailAccount.uidvalidity, current_uid_validity).set(
|
||||
EmailAccount.uidnext, uidnext
|
||||
).where(EmailAccount.name == self.settings.email_account_name).run()
|
||||
frappe.db.set_value(
|
||||
"Email Account",
|
||||
self.settings.email_account_name,
|
||||
{"uidvalidity": current_uid_validity, "uidnext": uidnext},
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
sync_count = 100 if uid_validity else int(self.settings.initial_sync_count)
|
||||
from_uid = 1 if uidnext < (sync_count + 1) or (uidnext - sync_count) < 1 else uidnext - sync_count
|
||||
|
|
@ -245,10 +250,7 @@ class EmailServer:
|
|||
pattern = rf"(?<={cmd} )[0-9]*"
|
||||
match = re.search(pattern, response.decode("utf-8"), re.U | re.I)
|
||||
|
||||
if match:
|
||||
return match.group(0)
|
||||
else:
|
||||
return None
|
||||
return match[0] if match else None
|
||||
|
||||
def retrieve_message(self, uid, msg_num):
|
||||
try:
|
||||
|
|
@ -267,7 +269,7 @@ class EmailServer:
|
|||
|
||||
except Exception as e:
|
||||
if self.has_login_limit_exceeded(e):
|
||||
raise LoginLimitExceeded(e)
|
||||
raise LoginLimitExceeded(e) from e
|
||||
|
||||
frappe.log_error("Unable to fetch email", self.make_error_msg(uid, msg_num))
|
||||
|
||||
|
|
@ -295,20 +297,18 @@ class EmailServer:
|
|||
with suppress(Exception):
|
||||
if not cint(self.settings.use_imap):
|
||||
self.pop.dele(msg_num)
|
||||
else:
|
||||
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
|
||||
if self.settings.email_sync_rule == "UNSEEN":
|
||||
self.imap.uid("STORE", uid, "+FLAGS", "(\\SEEN)")
|
||||
elif self.settings.email_sync_rule == "UNSEEN":
|
||||
self.imap.uid("STORE", uid, "+FLAGS", "(\\SEEN)")
|
||||
|
||||
def is_temporary_system_problem(self, e):
|
||||
messages = (
|
||||
"-ERR [SYS/TEMP] Temporary system problem. Please try again later.",
|
||||
"Connection timed out",
|
||||
)
|
||||
for message in messages:
|
||||
if message in strip(cstr(e)) or message in strip(cstr(getattr(e, "strerror", ""))):
|
||||
return True
|
||||
return False
|
||||
return any(
|
||||
message in strip(cstr(e)) or message in strip(cstr(getattr(e, "strerror", "")))
|
||||
for message in messages
|
||||
)
|
||||
|
||||
def make_error_msg(self, uid, msg_num):
|
||||
traceback = frappe.get_traceback(with_context=True)
|
||||
|
|
@ -322,14 +322,16 @@ class EmailServer:
|
|||
partial_mail = Email(headers)
|
||||
|
||||
if partial_mail:
|
||||
return (
|
||||
"\nDate: {date}\nFrom: {from_email}\nSubject: {subject}\n\n\nTraceback: \n{traceback}".format(
|
||||
date=partial_mail.date,
|
||||
from_email=partial_mail.from_email,
|
||||
subject=partial_mail.subject,
|
||||
traceback=traceback,
|
||||
)
|
||||
)
|
||||
return f"""
|
||||
Date: {partial_mail.date}
|
||||
From: {partial_mail.from_email}
|
||||
Subject: {partial_mail.subject}
|
||||
|
||||
|
||||
Traceback:
|
||||
{traceback}
|
||||
"""
|
||||
|
||||
return traceback
|
||||
|
||||
def update_flag(self, folder, uid_list=None):
|
||||
|
|
@ -415,10 +417,7 @@ class Email:
|
|||
|
||||
# Convert non-string (e.g. None)
|
||||
# Truncate to 140 chars (can be used as a document name)
|
||||
self.subject = str(self.subject).strip()[:140]
|
||||
|
||||
if not self.subject:
|
||||
self.subject = "No Subject"
|
||||
self.subject = str(self.subject).strip()[:140] or "No Subject"
|
||||
|
||||
def set_from(self):
|
||||
# gmail mailing-list compatibility
|
||||
|
|
@ -499,12 +498,7 @@ class Email:
|
|||
self.html_content += markdown(text_content)
|
||||
|
||||
def get_charset(self, part):
|
||||
"""Detect charset."""
|
||||
charset = part.get_content_charset()
|
||||
if not charset:
|
||||
charset = chardet.detect(safe_encode(cstr(part)))["encoding"]
|
||||
|
||||
return charset
|
||||
return part.get_content_charset() or chardet.detect(safe_encode(cstr(part)))["encoding"]
|
||||
|
||||
def get_payload(self, part):
|
||||
charset = self.get_charset(part)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ class TestWebhook(FrappeTestCase):
|
|||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# delete any existing webhooks
|
||||
frappe.db.rollback()
|
||||
frappe.db.delete("Webhook")
|
||||
frappe.db.commit()
|
||||
|
||||
def setUp(self):
|
||||
# retrieve or create a User webhook for `after_insert`
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1034,21 +1034,17 @@ class DatabaseQuery:
|
|||
self._fetch_shared_documents = True
|
||||
self.match_filters.append(match_filters)
|
||||
|
||||
def get_permission_query_conditions(self):
|
||||
def get_permission_query_conditions(self) -> str:
|
||||
conditions = []
|
||||
hooks = frappe.get_hooks("permission_query_conditions", {})
|
||||
condition_methods = hooks.get(self.doctype, []) + hooks.get("*", [])
|
||||
if condition_methods:
|
||||
for method in condition_methods:
|
||||
c = frappe.call(frappe.get_attr(method), self.user, doctype=self.doctype)
|
||||
if c:
|
||||
conditions.append(c)
|
||||
for method in condition_methods:
|
||||
if c := frappe.call(frappe.get_attr(method), self.user, doctype=self.doctype):
|
||||
conditions.append(c)
|
||||
|
||||
permision_script_name = get_server_script_map().get("permission_query", {}).get(self.doctype)
|
||||
if permision_script_name:
|
||||
script = frappe.get_doc("Server Script", permision_script_name)
|
||||
condition = script.get_permission_query_conditions(self.user)
|
||||
if condition:
|
||||
if permission_script_name := get_server_script_map().get("permission_query", {}).get(self.doctype):
|
||||
script = frappe.get_doc("Server Script", permission_script_name)
|
||||
if condition := script.get_permission_query_conditions(self.user):
|
||||
conditions.append(condition)
|
||||
|
||||
return " and ".join(conditions) if conditions else ""
|
||||
|
|
|
|||
|
|
@ -441,9 +441,6 @@ def has_controller_permissions(doc, ptype, user=None, debug=False) -> bool:
|
|||
hooks = frappe.get_hooks("has_permission")
|
||||
methods = hooks.get(doc.doctype, []) + hooks.get("*", [])
|
||||
|
||||
if not methods:
|
||||
return True
|
||||
|
||||
for method in reversed(methods):
|
||||
controller_permission = frappe.call(method, doc=doc, ptype=ptype, user=user, debug=debug)
|
||||
debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")
|
||||
|
|
|
|||
|
|
@ -198,8 +198,9 @@
|
|||
"icon": "fa fa-font",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 3,
|
||||
"modified": "2024-03-23 16:03:28.922006",
|
||||
"modified": "2024-04-12 10:30:25.793932",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Letter Head",
|
||||
|
|
|
|||
|
|
@ -260,14 +260,14 @@ function delete_column(with_children) {
|
|||
const options = computed(() => {
|
||||
let groups = [
|
||||
{
|
||||
group: "Section",
|
||||
group: __("Section"),
|
||||
items: [
|
||||
{ label: __("Add section below"), onClick: add_section_below },
|
||||
{ label: __("Remove section"), onClick: remove_section },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Column",
|
||||
group: __("Column"),
|
||||
items: [{ label: __("Add column"), onClick: add_column }],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -304,27 +304,40 @@ frappe.unscrub = function (txt) {
|
|||
return frappe.model.unscrub(txt);
|
||||
};
|
||||
|
||||
frappe.get_data_pill = (label, target_id = null, remove_action = null, image = null) => {
|
||||
frappe.get_data_pill = (
|
||||
label,
|
||||
target_id = null,
|
||||
remove_action = null,
|
||||
image = null,
|
||||
colored = false
|
||||
) => {
|
||||
let color = "",
|
||||
style = "";
|
||||
if (colored) {
|
||||
color = frappe.get_palette(label);
|
||||
}
|
||||
style = `background-color: var(${color[0]}); color: var(${color[1]})`;
|
||||
let data_pill_wrapper = $(`
|
||||
<button class="data-pill btn">
|
||||
<button class="data-pill btn" style="${style}">
|
||||
<div class="flex align-center ellipsis">
|
||||
${image ? image : ""}
|
||||
<span class="pill-label">${label}</span>
|
||||
<span class="pill-label">${label} </span>
|
||||
</div>
|
||||
</button>
|
||||
`);
|
||||
|
||||
if (remove_action) {
|
||||
let remove_btn = $(`
|
||||
<span class="remove-btn cursor-pointer">
|
||||
${frappe.utils.icon("close", "sm")}
|
||||
</span>
|
||||
`).click(() => {
|
||||
remove_action(target_id || label, data_pill_wrapper);
|
||||
});
|
||||
`);
|
||||
if (typeof remove_action === "function") {
|
||||
remove_btn.click(() => {
|
||||
remove_action(target_id || label, data_pill_wrapper);
|
||||
});
|
||||
}
|
||||
data_pill_wrapper.append(remove_btn);
|
||||
}
|
||||
|
||||
return data_pill_wrapper;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,9 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
|
|||
|
||||
const ace_language_mode = language_map[language] || "";
|
||||
this.editor.session.setMode(ace_language_mode);
|
||||
this.editor.setKeyboardHandler("ace/keyboard/vscode");
|
||||
this.editor.setKeyboardHandler(
|
||||
`ace/keyboard/${frappe.boot.user.code_editor_type || "vscode"}`
|
||||
);
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
|
|
|
|||
|
|
@ -219,10 +219,13 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
) {
|
||||
html += '<br><span class="small">' + __(d.description) + "</span>";
|
||||
}
|
||||
return $("<li></li>")
|
||||
return $(`<div role="option">`)
|
||||
.on("click", (event) => {
|
||||
me.awesomplete.select(event.currentTarget, event.currentTarget);
|
||||
})
|
||||
.data("item.autocomplete", d)
|
||||
.prop("aria-selected", "false")
|
||||
.html(`<a><p title="${frappe.utils.escape_html(_label)}">${html}</p></a>`)
|
||||
.html(`<p title="${frappe.utils.escape_html(_label)}">${html}</p>`)
|
||||
.get(0);
|
||||
},
|
||||
sort: function () {
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@ export default class GridRow {
|
|||
);
|
||||
if (selectedColumn && !selectedColumn.hidden && show_field(selectedColumn.fieldtype)) {
|
||||
fields.push({
|
||||
label: selectedColumn.label,
|
||||
label: __(selectedColumn.label, null, this.grid.doctype),
|
||||
value: selectedColumn.fieldname,
|
||||
checked: true,
|
||||
});
|
||||
|
|
@ -519,7 +519,7 @@ export default class GridRow {
|
|||
show_field(column.fieldtype)
|
||||
) {
|
||||
fields.push({
|
||||
label: column.label,
|
||||
label: __(column.label, null, this.grid.doctype),
|
||||
value: column.fieldname,
|
||||
checked: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export default class ListSettings {
|
|||
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle " + show_sortable_handle)}
|
||||
</div>
|
||||
<div class="col-10" style="padding-left:0px;">
|
||||
${me.fields[idx].label}
|
||||
${__(me.fields[idx].label, null, me.doctype)}
|
||||
</div>
|
||||
<div class="col-1 ${can_remove}">
|
||||
<a class="text-muted remove-field" data-fieldname="${me.fields[idx].fieldname}">
|
||||
|
|
@ -132,14 +132,14 @@ export default class ListSettings {
|
|||
fields_html.html(`
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label" style="padding-right: 0px;">Fields</label>
|
||||
<label class="control-label" style="padding-right: 0px;">${__("Fields")}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
${fields}
|
||||
</div>
|
||||
<p class="help-box small text-muted">
|
||||
<a class="add-new-fields text-muted">
|
||||
+ Add / Remove Fields
|
||||
${__("+ Add / Remove Fields")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -209,7 +209,7 @@ export default class ListSettings {
|
|||
for (let idx = 0; idx < fields_order.length; idx++) {
|
||||
me.fields.push({
|
||||
fieldname: fields_order.item(idx).getAttribute("data-fieldname"),
|
||||
label: fields_order.item(idx).getAttribute("data-label"),
|
||||
label: __(fields_order.item(idx).getAttribute("data-label")),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ export default class ListSettings {
|
|||
let field = frappe.meta.get_docfield(me.doctype, value);
|
||||
if (field) {
|
||||
me.fields.push({
|
||||
label: field.label,
|
||||
label: __(field.label, null, me.doctype),
|
||||
fieldname: field.fieldname,
|
||||
});
|
||||
}
|
||||
|
|
@ -320,7 +320,7 @@ export default class ListSettings {
|
|||
me.subject_field.fieldname != field.fieldname
|
||||
) {
|
||||
me.fields.push({
|
||||
label: field.label,
|
||||
label: __(field.label, null, me.doctype),
|
||||
fieldname: field.fieldname,
|
||||
});
|
||||
}
|
||||
|
|
@ -331,7 +331,7 @@ export default class ListSettings {
|
|||
let me = this;
|
||||
|
||||
me.subject_field = {
|
||||
label: "ID",
|
||||
label: __("ID"),
|
||||
fieldname: "name",
|
||||
};
|
||||
|
||||
|
|
@ -339,7 +339,7 @@ export default class ListSettings {
|
|||
let field = frappe.meta.get_docfield(me.doctype, meta.title_field.trim());
|
||||
|
||||
me.subject_field = {
|
||||
label: field.label,
|
||||
label: __(field.label, null, me.doctype),
|
||||
fieldname: field.fieldname,
|
||||
};
|
||||
}
|
||||
|
|
@ -353,7 +353,7 @@ export default class ListSettings {
|
|||
if (frappe.has_indicator(me.doctype)) {
|
||||
me.fields.push({
|
||||
type: "Status",
|
||||
label: "Status",
|
||||
label: __("Status"),
|
||||
fieldname: "status_field",
|
||||
});
|
||||
}
|
||||
|
|
@ -365,7 +365,7 @@ export default class ListSettings {
|
|||
meta.fields.forEach((field) => {
|
||||
if (!frappe.model.no_value_type.includes(field.fieldtype)) {
|
||||
multiselect_fields.push({
|
||||
label: field.label,
|
||||
label: __(field.label, null, field.doctype),
|
||||
value: field.fieldname,
|
||||
checked: fields.includes(field.fieldname),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -177,13 +177,12 @@ frappe.router = {
|
|||
// /app/user/user-001 = ["Form", "User", "user-001"]
|
||||
// /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"]
|
||||
|
||||
let private_workspace = route[1] && `${route[1]}-${frappe.user.name.toLowerCase()}`;
|
||||
|
||||
if (frappe.workspaces[route[0]]) {
|
||||
// public workspace
|
||||
route = ["Workspaces", frappe.workspaces[route[0]].title];
|
||||
} else if (route[0] == "private") {
|
||||
// private workspace
|
||||
let private_workspace = route[1] && `${route[1]}-${frappe.user.name.toLowerCase()}`;
|
||||
if (!frappe.workspaces[private_workspace] && localStorage.new_workspace) {
|
||||
let new_workspace = JSON.parse(localStorage.new_workspace);
|
||||
if (frappe.router.slug(new_workspace.title) === route[1]) {
|
||||
|
|
@ -474,26 +473,26 @@ frappe.router = {
|
|||
return "/app/" + path_string;
|
||||
}
|
||||
|
||||
// Workspace
|
||||
// Resolution order
|
||||
// 1. User's default workspace in user doctype
|
||||
// 2. Private home
|
||||
// 3. Public home
|
||||
// 4. First workspace in list
|
||||
let private_home = `home-${frappe.user.name.toLowerCase()}`;
|
||||
let default_page = null;
|
||||
if (frappe.boot.user.default_workspace) {
|
||||
default_page = frappe.router.slug(frappe.boot.user.default_workspace.name);
|
||||
} else if (frappe.workspaces[private_home]) {
|
||||
default_page = private_home;
|
||||
} else if (frappe.workspaces["home"]) {
|
||||
default_page = "home";
|
||||
} else {
|
||||
// Fallback to first workspace
|
||||
default_page = Object.keys(frappe.workspaces)[0];
|
||||
}
|
||||
let default_workspace = frappe.router.slug(frappe.boot.user.default_workspace?.name || "");
|
||||
|
||||
if (frappe.workspaces[default_page]?.public == false) {
|
||||
default_page = "private/" + default_page;
|
||||
}
|
||||
let workspace =
|
||||
frappe.workspaces[default_workspace] ||
|
||||
frappe.workspaces[private_home] ||
|
||||
frappe.workspaces["home"] ||
|
||||
Object.values(frappe.workspaces)[0];
|
||||
|
||||
if (default_page) {
|
||||
return "/app/" + default_page;
|
||||
if (workspace) {
|
||||
return (
|
||||
"/app/" +
|
||||
(workspace.public ? "" : "private/") +
|
||||
frappe.router.slug(workspace.title)
|
||||
);
|
||||
}
|
||||
|
||||
return "/app";
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ frappe.ui.FilterGroup = class {
|
|||
update_filter_button() {
|
||||
const filters_applied = this.filters.length > 0;
|
||||
const button_label = filters_applied
|
||||
? __("Filters <span class='filter-label'>{0}</span>", [this.filters.length])
|
||||
? __("Filters {0}", [`<span class="filter-label">${this.filters.length}</span>`])
|
||||
: __("Filter");
|
||||
|
||||
this.filter_button
|
||||
|
|
|
|||
|
|
@ -100,17 +100,22 @@ frappe.ui.Tags = class {
|
|||
}
|
||||
|
||||
get_tag(label) {
|
||||
let $tag = frappe.get_data_pill(label, label, (target, pill_wrapper) => {
|
||||
this.removeTag(target);
|
||||
pill_wrapper.closest(".form-tag-row").remove();
|
||||
});
|
||||
|
||||
let colored = true;
|
||||
let $tag = frappe.get_data_pill(
|
||||
label,
|
||||
label,
|
||||
(target, pill_wrapper) => {
|
||||
this.removeTag(target);
|
||||
pill_wrapper.closest(".form-tag-row").remove();
|
||||
},
|
||||
null,
|
||||
colored
|
||||
);
|
||||
if (this.onTagClick) {
|
||||
$tag.on("click", ".pill-label", () => {
|
||||
this.onTagClick(label);
|
||||
});
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ frappe.tools.downloadify = function (data, roles, title) {
|
|||
|
||||
frappe.markdown = function (txt) {
|
||||
if (!frappe.md2html) {
|
||||
frappe.md2html = new showdown.Converter();
|
||||
frappe.md2html = new showdown.Converter({ tables: true });
|
||||
}
|
||||
|
||||
while (txt.substr(0, 1) === "\n") {
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ frappe.views.Workspace = class Workspace {
|
|||
|
||||
if (frappe.boot.user.default_workspace) {
|
||||
default_page = {
|
||||
name: frappe.boot.user.default_workspace.name,
|
||||
name: frappe.boot.user.default_workspace.title,
|
||||
public: frappe.boot.user.default_workspace.public,
|
||||
};
|
||||
} else if (
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ let properties = computed(() => {
|
|||
if (field.val() === "") field.focus();
|
||||
});
|
||||
if (store.workflow.selected && "action" in store.workflow.selected.data) {
|
||||
title.value = "Transition Properties";
|
||||
title.value = __("Transition Properties");
|
||||
return store.transitionfields.filter((df) =>
|
||||
["action", "allowed", "allow_self_approval", "condition"].includes(df.fieldname)
|
||||
);
|
||||
} else if (store.workflow.selected && "state" in store.workflow.selected.data) {
|
||||
title.value = "State Properties";
|
||||
title.value = __("State Properties");
|
||||
let allow_edit = store.statefields.find((df) => df.fieldname == "allow_edit");
|
||||
store.statefields = store.statefields.filter(
|
||||
(df) => !["allow_edit", "workflow_builder_id"].includes(df.fieldname)
|
||||
|
|
@ -39,7 +39,7 @@ let properties = computed(() => {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
title.value = "Workflow Details";
|
||||
title.value = __("Workflow Details");
|
||||
return store.workflowfields.filter(
|
||||
(df) => !["states", "transitions", "workflow_data", "workflow_name"].includes(df.fieldname)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
const workflow_data = clean_workflow_data();
|
||||
doc.workflow_data = JSON.stringify(workflow_data);
|
||||
await frappe.call("frappe.client.save", { doc });
|
||||
frappe.toast("Workflow updated successfully");
|
||||
frappe.toast(__("Workflow updated successfully"));
|
||||
fetch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
& > ul:empty {
|
||||
display: none;
|
||||
}
|
||||
& > [role="listbox"] {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > ul {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
|
|
@ -33,7 +33,8 @@
|
|||
z-index: 4;
|
||||
min-width: 250px;
|
||||
|
||||
& > li {
|
||||
& > li,
|
||||
& > [role="option"] {
|
||||
cursor: pointer;
|
||||
@include get_textstyle("sm", "regular");
|
||||
padding: var(--padding-sm);
|
||||
|
|
@ -52,17 +53,17 @@
|
|||
strong {
|
||||
@include get_textstyle("sm", "bold");
|
||||
}
|
||||
}
|
||||
|
||||
& > li .link-option {
|
||||
font-weight: normal;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.link-option {
|
||||
font-weight: normal;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
& > li:hover,
|
||||
& > li[aria-selected="true"] {
|
||||
background-color: var(--awesomplete-hover-bg);
|
||||
color: var(--text-color);
|
||||
&:hover,
|
||||
&[aria-selected="true"] {
|
||||
background-color: var(--awesomplete-hover-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
|
|
|
|||
|
|
@ -381,6 +381,17 @@ textarea.form-control {
|
|||
min-height: 300px;
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: var(--padding-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-toggle,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ body.modal-open[style^="padding-right"] {
|
|||
}
|
||||
}
|
||||
|
||||
.awesomplete ul {
|
||||
.awesomplete ul,
|
||||
.awesomplete [role="listbox"] {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,11 +31,13 @@
|
|||
}
|
||||
|
||||
.awesomplete {
|
||||
ul[role="listbox"] {
|
||||
& > ul,
|
||||
& > [role="listbox"] {
|
||||
min-width: 100%;
|
||||
width: auto;
|
||||
|
||||
li {
|
||||
& > li,
|
||||
& > [role="option"] {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ input.list-header-checkbox {
|
|||
margin-bottom: 0;
|
||||
--checkbox-right-margin: 0;
|
||||
background-color: var(--card-bg);
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
// Extend the checkbox's clickable area
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ body {
|
|||
// overflow-x: hidden; //Prevent scroll on narrow devices
|
||||
// }
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
body[data-route^="Form"] {
|
||||
.page-title .title-text {
|
||||
max-width: 35vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.intro-area,
|
||||
.footnote-area {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
cursor: pointer;
|
||||
margin-bottom: 0px;
|
||||
margin-right: var(--margin-sm);
|
||||
max-width: 48vw;
|
||||
max-width: 25vw;
|
||||
}
|
||||
.indicator-pill {
|
||||
margin-top: 2px;
|
||||
|
|
|
|||
|
|
@ -143,18 +143,7 @@ def install_basic_docs():
|
|||
|
||||
|
||||
def get_admin_password():
|
||||
def ask_admin_password():
|
||||
admin_password = getpass.getpass("Set Administrator password: ")
|
||||
admin_password2 = getpass.getpass("Re-enter Administrator password: ")
|
||||
if admin_password != admin_password2:
|
||||
print("\nPasswords do not match")
|
||||
return ask_admin_password()
|
||||
return admin_password
|
||||
|
||||
admin_password = frappe.conf.get("admin_password")
|
||||
if not admin_password:
|
||||
return ask_admin_password()
|
||||
return admin_password
|
||||
return frappe.conf.get("admin_password") or getpass.getpass("Set Administrator password: ")
|
||||
|
||||
|
||||
def before_tests():
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ class UserPermissions:
|
|||
[
|
||||
"creation",
|
||||
"desk_theme",
|
||||
"code_editor_type",
|
||||
"document_follow_notify",
|
||||
"email",
|
||||
"email_signature",
|
||||
|
|
@ -235,8 +236,12 @@ class UserPermissions:
|
|||
self.build_permissions()
|
||||
|
||||
if d.get("default_workspace"):
|
||||
public = frappe.get_cached_value("Workspace", d.default_workspace, "public")
|
||||
d.default_workspace = {"name": d.default_workspace, "public": public}
|
||||
workspace = frappe.get_cached_doc("Workspace", d.default_workspace)
|
||||
d.default_workspace = {
|
||||
"name": workspace.name,
|
||||
"public": workspace.public,
|
||||
"title": workspace.title,
|
||||
}
|
||||
|
||||
d.name = self.name
|
||||
d.onboarding_status = frappe.parse_json(d.onboarding_status)
|
||||
|
|
|
|||
|
|
@ -652,52 +652,34 @@ def get_in_list_view_fields(doctype):
|
|||
return [get_field_df(f) for f in fields]
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=False):
|
||||
web_form_doc = frappe.get_doc("Web Form", web_form_name)
|
||||
doctype_validated = False
|
||||
limited_to_user = False
|
||||
if web_form_doc.login_required:
|
||||
# check if frappe session user is not guest or admin
|
||||
if frappe.session.user != "Guest":
|
||||
doctype_validated = True
|
||||
web_form: WebForm = frappe.get_doc("Web Form", web_form_name)
|
||||
|
||||
if not allow_read_on_all_link_options:
|
||||
limited_to_user = True
|
||||
else:
|
||||
frappe.throw(_("You must be logged in to use this form."), frappe.PermissionError)
|
||||
if web_form.login_required and frappe.session.user == "Guest":
|
||||
frappe.throw(_("You must be logged in to use this form."), frappe.PermissionError)
|
||||
|
||||
else:
|
||||
for field in web_form_doc.web_form_fields:
|
||||
if field.options == doctype:
|
||||
doctype_validated = True
|
||||
break
|
||||
|
||||
if doctype_validated:
|
||||
link_options, filters = [], {}
|
||||
|
||||
if limited_to_user:
|
||||
filters = {"owner": frappe.session.user}
|
||||
|
||||
fields = ["name as value"]
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if meta.title_field and meta.show_title_field_in_link:
|
||||
fields.append(f"{meta.title_field} as label")
|
||||
|
||||
link_options = frappe.get_all(doctype, filters, fields)
|
||||
|
||||
if meta.title_field and meta.show_title_field_in_link:
|
||||
return json.dumps(link_options, default=str)
|
||||
else:
|
||||
return "\n".join([str(doc.value) for doc in link_options])
|
||||
|
||||
else:
|
||||
raise frappe.PermissionError(
|
||||
_("You don't have permission to access the {0} DocType.").format(doctype)
|
||||
if not web_form.published or not any(f for f in web_form.web_form_fields if f.options == doctype):
|
||||
frappe.throw(
|
||||
_("You don't have permission to access the {0} DocType.").format(doctype), frappe.PermissionError
|
||||
)
|
||||
|
||||
link_options, filters = [], {}
|
||||
if not allow_read_on_all_link_options:
|
||||
filters = {"owner": frappe.session.user}
|
||||
|
||||
fields = ["name as value"]
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
if meta.title_field and meta.show_title_field_in_link:
|
||||
fields.append(f"{meta.title_field} as label")
|
||||
|
||||
link_options = frappe.get_all(doctype, filters, fields)
|
||||
|
||||
if meta.title_field and meta.show_title_field_in_link:
|
||||
return json.dumps(link_options, default=str)
|
||||
else:
|
||||
return "\n".join([str(doc.value) for doc in link_options])
|
||||
|
||||
|
||||
@redis_cache(ttl=60 * 60)
|
||||
def get_published_web_forms() -> dict[str, str]:
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype === 'Link'",
|
||||
"depends_on": "eval:doc.fieldtype === 'Link' && parent.login_required",
|
||||
"fieldname": "allow_read_on_all_link_options",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Read On All Link Options"
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:04:02.173108",
|
||||
"modified": "2024-04-15 16:11:58.469820",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Form Field",
|
||||
|
|
|
|||
|
|
@ -338,7 +338,8 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:04:02.407199",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-04-12 10:30:49.022735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Page",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ dependencies = [
|
|||
"psutil~=5.9.5",
|
||||
"psycopg2-binary~=2.9.1",
|
||||
"pyOpenSSL~=24.0.0",
|
||||
"pydantic==2.3.0",
|
||||
"pydantic~=2.7.0",
|
||||
"pyotp~=2.8.0",
|
||||
"python-dateutil~=2.8.2",
|
||||
"pytz==2023.3",
|
||||
|
|
@ -67,8 +67,8 @@ dependencies = [
|
|||
"rsa>=4.1",
|
||||
"semantic-version~=2.10.0",
|
||||
"sentry-sdk~=1.37.1",
|
||||
"sqlparse~=0.4.4",
|
||||
"sql_metadata~=2.9.0",
|
||||
"sqlparse~=0.5.0",
|
||||
"sql_metadata~=2.11.0",
|
||||
"tenacity~=8.2.2",
|
||||
"terminaltables~=3.1.10",
|
||||
"traceback-with-variables~=2.0.4",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue