Merge remote-tracking branch 'upstream/develop' into list-padding

This commit is contained in:
barredterra 2024-04-16 19:49:54 +02:00
commit 75f588d386
39 changed files with 588 additions and 430 deletions

View file

@ -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,
});

View file

@ -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": []
}
}

View file

@ -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
}
}

View file

@ -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"]

View file

@ -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
}
}

View file

@ -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(

View file

@ -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",

View file

@ -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)

View file

@ -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

View file

@ -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 ""

View file

@ -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}")

View file

@ -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",

View file

@ -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 }],
},
];

View file

@ -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;
};

View file

@ -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) {

View file

@ -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 () {

View file

@ -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,
});

View file

@ -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),
});

View file

@ -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";

View file

@ -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

View file

@ -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;
}
};

View file

@ -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") {

View file

@ -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 (

View file

@ -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)
);

View file

@ -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);

View file

@ -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 {

View file

@ -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,

View file

@ -85,7 +85,8 @@ body.modal-open[style^="padding-right"] {
}
}
.awesomplete ul {
.awesomplete ul,
.awesomplete [role="listbox"] {
z-index: 2;
}

View file

@ -31,11 +31,13 @@
}
.awesomplete {
ul[role="listbox"] {
& > ul,
& > [role="listbox"] {
min-width: 100%;
width: auto;
li {
& > li,
& > [role="option"] {
max-width: 300px;
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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():

View file

@ -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)

View file

@ -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]:

View file

@ -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",

View file

@ -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",

View file

@ -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",