diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index aff4c18b32..f2790166f9 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -65,7 +65,7 @@ context("Data Control", () => { .should("have.class", "reqd"); //Checking if the status is "Not Saved" initially - cy.get(".indicator-pill").should("have.text", "Not Saved"); + cy.get(".page-head-content .indicator-pill").should("have.text", "Not Saved"); //Inputting data in the field cy.fill_field("name1", "@@###", "Data"); diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 7ef59cc20b..2b190095ee 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -76,6 +76,7 @@ "width", "max_height", "columns", + "icon", "column_break_22", "description", "documentation_url", @@ -626,6 +627,12 @@ "fieldtype": "Select", "label": "Button Color", "options": "\nDefault\nPrimary\nInfo\nSuccess\nWarning\nDanger" + }, + { + "depends_on": "eval: doc.type == \"Tab Break\"", + "fieldname": "icon", + "fieldtype": "Icon", + "label": "Icon" } ], "grid_page_length": 50, @@ -633,7 +640,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-12-01 06:56:29.335491", + "modified": "2025-12-15 11:28:13.492477", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index c3c85c6269..44c9ade9a8 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -182,7 +182,7 @@ def get_milestones(doctype, name): def get_attachments(dt, dn): return frappe.get_all( "File", - fields=["name", "file_name", "file_url", "is_private"], + fields=["name", "file_name", "file_url", "is_private", "file_type", "file_size"], filters={"attached_to_name": str(dn), "attached_to_doctype": dt}, ) diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index f2c405e3d5..a49a6aca1e 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -312,8 +312,8 @@ frappe.get_data_pill = ( style = ""; if (colored) { color = frappe.get_palette(label); + style = `background-color: var(${color[0]}); color: var(${color[1]})`; } - style = `background-color: var(${color[0]}); color: var(${color[1]})`; let data_pill_wrapper = $(` + `); + btn_group.find(".complete-btn").click(() => { + this.close_assignment(assignment).then((assignments) => { + row.remove(); + this.render(assignments); + }); + }); + + if (in_dialogue) { + const html = this.assignment_details + .filter((x) => x.owner === assignment && strip_html(x.description)) + .map((x) => x.description) + .join("
"); + if (html) { + let description = $( + "
" + ).html(html); + row.find(".assignee").append(description); + } + } + } + + if (assignment === frappe.session.user || this.frm.perm[0].write) { + btn_group.append(` + + `); + btn_group.find(".remove-btn").click(() => { + this.remove_assignment(assignment).then((assignments) => { + row.remove(); + this.render(assignments); + }); + }); + } + return row; + } + + remove_assignment(assignment) { + return frappe.xcall("frappe.desk.form.assign_to.remove", { + doctype: this.frm.doctype, + name: this.frm.docname, + assign_to: assignment, + }); + } + + close_assignment(assignment) { + return frappe.xcall("frappe.desk.form.assign_to.close", { + doctype: this.frm.doctype, + name: this.frm.docname, + assign_to: assignment, + }); + } + + render(assignments) { + this.frm && this.frm.assign_to.render(assignments); + } +}; + +frappe.ui.form.AssignmentDialog = class extends frappe.ui.form.AssignmentClass { + constructor(opts) { + super(opts); this.make(); } @@ -294,9 +401,6 @@ frappe.ui.form.AssignmentDialog = class { }); this.dialog.show(); } - render(assignments) { - this.frm && this.frm.assign_to.render(assignments); - } add_assignment(assignment) { return frappe .xcall("frappe.desk.form.assign_to.add", { @@ -309,79 +413,10 @@ frappe.ui.form.AssignmentDialog = class { this.render(assignments); }); } - remove_assignment(assignment) { - return frappe.xcall("frappe.desk.form.assign_to.remove", { - doctype: this.frm.doctype, - name: this.frm.docname, - assign_to: assignment, - }); - } - close_assignment(assignment) { - return frappe.xcall("frappe.desk.form.assign_to.close", { - doctype: this.frm.doctype, - name: this.frm.docname, - assign_to: assignment, - }); - } update_assignment(assignment) { const in_the_list = this.assignment_list.find(`[data-user="${assignment}"]`).length; if (!in_the_list) { - this.assignment_list.append(this.get_assignment_row(assignment)); + this.assignment_list.append(this.get_assignment_row(assignment, true)); } } - get_assignment_row(assignment) { - const row = $(` -
-
- ${frappe.avatar(assignment)} - ${frappe.user.full_name(assignment)} -
-
-
-
- `); - - const btn_group = row.find(".btn-group"); - - if (assignment === frappe.session.user) { - btn_group.append(` - - `); - btn_group.find(".complete-btn").click(() => { - this.close_assignment(assignment).then((assignments) => { - row.remove(); - this.render(assignments); - }); - }); - - const html = this.assignment_details - .filter((x) => x.owner === assignment && strip_html(x.description)) - .map((x) => x.description) - .join("
"); - if (html) { - $( - "
" - ) - .html(html) - .appendTo(row); - } - } - - if (assignment === frappe.session.user || this.frm.perm[0].write) { - btn_group.append(` - - `); - btn_group.find(".remove-btn").click(() => { - this.remove_assignment(assignment).then((assignments) => { - row.remove(); - this.render(assignments); - }); - }); - } - return row; - } }; diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index e9baddb7ab..5cfeed4292 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -113,47 +113,68 @@ frappe.ui.form.Attachments = class Attachments { } add_attachment(attachment) { - var file_name = attachment.file_name; - var file_url = this.get_file_url(attachment); - var fileid = attachment.name; - if (!file_name) { - file_name = file_url; - } + let file_name = attachment.file_name || this.get_file_url(attachment); + let file_url = this.get_file_url(attachment); + let fileid = attachment.name; + let me = this; - var me = this; + let $attachment_action = $(`
`); - let file_label = ` -
+ let $file_label = $(` + ${frappe.utils.xss_sanitise(file_name)} - `; + + `); + + $attachment_action.append($file_label); - let remove_action = null; if (this.can_delete_attachment()) { - remove_action = function (target_id) { - frappe.confirm(__("Are you sure you want to delete the attachment?"), function () { - let target_attachment = me - .get_attachments() - .find((attachment) => attachment.name === target_id); - let to_be_removed = me - .get_attachments() - .filter( - (attachment) => attachment.file_name === target_attachment.file_name - ); - to_be_removed.forEach((attachment) => me.remove_attachment(attachment.name)); - }); - return false; - }; + let $delete_attachment = $(` + + `); + + $delete_attachment.on("click", () => { + me.delete_attachment(fileid); + }); + + $attachment_action.append($delete_attachment); } - const icon = ` - ${frappe.utils.icon(attachment.is_private ? "es-line-lock" : "es-line-unlock", "sm ml-0")} - `; + const $attachment_meta = $(` +
+ + ${frappe.utils.icon(attachment.is_private ? "es-line-lock" : "es-line-unlock", "sm ml-0")} + + ${attachment.file_type} + + ${frappe.form.formatters.FileSize(attachment.file_size)} + +
+ `); - $(`
`) - .append(frappe.get_data_pill(file_label, fileid, remove_action, icon)) - .insertAfter(this.add_attachment_wrapper); + // Final row + let $row = $('
'); + $row.append($attachment_action); + $row.append($attachment_meta); + + $row.insertAfter(this.add_attachment_wrapper); + } + + delete_attachment(target_id) { + let me = this; + frappe.confirm(__("Are you sure you want to delete the attachment?"), function () { + let target_attachment = me + .get_attachments() + .find((attachment) => attachment.name === target_id); + let to_be_removed = me + .get_attachments() + .filter((attachment) => attachment.file_name === target_attachment.file_name); + to_be_removed.forEach((attachment) => me.remove_attachment(attachment.name)); + }); } can_delete_attachment() { diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index 0f2c27ef3a..18ea6e490b 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -16,6 +16,7 @@ frappe.ui.form.Sidebar = class { doctype: this.frm.doctype, frm: this.frm, can_write: frappe.model.can_write(this.frm.doctype, this.frm.docname), + image_field: this.frm.meta.image_field ?? false, }); this.sidebar = $('') @@ -34,10 +35,27 @@ frappe.ui.form.Sidebar = class { this.setup_keyboard_shortcuts(); this.show_auto_repeat_status(); frappe.ui.form.setup_user_image_event(this.frm); - + this.indicator = $(this.sidebar).find(".sidebar-meta-details .indicator-pill"); + this.set_form_indicator(); + this.setup_copy_event(); + this.make_like(); this.refresh(); } + set_form_indicator() { + let indicator = frappe.get_indicator(this.frm.doc); + if (indicator) { + this.set_indicator(indicator[0], indicator[1]); + } + } + set_indicator(label, color) { + this.clear_indicator().removeClass("hide").html(`${label}`).addClass(color); + } + + clear_indicator() { + return this.indicator.addClass("indicator-pill no-indicator-dot whitespace-nowrap hide"); + } + setup_keyboard_shortcuts() { // add assignment shortcut let assignment_link = this.sidebar.find(".add-assignment"); @@ -61,6 +79,45 @@ frappe.ui.form.Sidebar = class { this.refresh_creation_modified(); frappe.ui.form.set_user_image(this.frm); } + this.refresh_like(); + } + + setup_copy_event() { + $(this.sidebar) + .find(".sidebar-meta-details .form-name-copy") + .on("click", (e) => { + frappe.utils.copy_to_clipboard($(e.currentTarget).attr("data-copy")); + }); + } + + make_like() { + this.like_wrapper = this.sidebar.find(".liked-by"); + this.like_icon = this.sidebar.find(".liked-by .like-icon"); + this.like_count = this.sidebar.find(".liked-by .like-count"); + frappe.ui.setup_like_popover(this.sidebar.find(".form-stats-likes"), ".like-icon"); + + this.like_icon.on("click", () => { + frappe.ui.toggle_like(this.like_wrapper, this.frm.doctype, this.frm.doc.name, () => { + this.refresh_like(); + }); + }); + } + + refresh_like() { + if (!this.like_icon) { + return; + } + + this.like_wrapper.attr("data-liked-by", this.frm.doc._liked_by); + const liked = frappe.ui.is_liked(this.frm.doc); + + this.like_wrapper + .toggleClass("not-liked", !liked) + .toggleClass("liked", liked) + .attr("data-doctype", this.frm.doctype) + .attr("data-name", this.frm.doc.name); + + this.like_count && this.like_count.text(JSON.parse(this.frm.doc._liked_by || "[]").length); } refresh_web_view_count() { @@ -76,62 +133,28 @@ frappe.ui.form.Sidebar = class { } refresh_creation_modified() { - let user_list = [this.frm.doc.owner, this.frm.doc.modified_by]; - if (this.frm.doc.owner === this.frm.doc.modified_by) { - user_list = [this.frm.doc.owner]; - } - - let avatar_group = frappe.avatar_group(user_list, 5, { - align: "left", - overlap: true, - }); - - this.sidebar.find(".created-modified-section").append(avatar_group); - - let creation_message = - get_user_message( - this.frm.doc.owner, - __("You created this", null), - __("{0} created this", [get_user_link(this.frm.doc.owner)]) - ) + - " · " + - cint(frappe.boot.user.show_absolute_datetime_in_timeline) || - cint(frappe.boot.sysdefaults.show_absolute_datetime_in_timeline) - ? frappe.datetime.str_to_user(this.frm.doc.creation) - : comment_when(this.frm.doc.creation); - let modified_message = - get_user_message( - this.frm.doc.modified_by, - __("You last edited this", null), - __("{0} last edited this", [get_user_link(this.frm.doc.modified_by)]) - ) + - " · " + - cint(frappe.boot.user.show_absolute_datetime_in_timeline) || - cint(frappe.boot.sysdefaults.show_absolute_datetime_in_timeline) - ? frappe.datetime.str_to_user(this.frm.doc.modified) - : comment_when(this.frm.doc.modified); - - if (user_list.length === 1) { - // same user created and edited - - avatar_group.find(".avatar").popover({ - trigger: "hover", - html: true, - content: creation_message + "
" + modified_message, - }); - } else { - avatar_group.find(".avatar:first-child").popover({ - trigger: "hover", - html: true, - content: creation_message, - }); - - avatar_group.find(".avatar:last-child").popover({ - trigger: "hover", - html: true, - content: modified_message, - }); - } + this.sidebar + .find(".modified-by") + .html( + get_user_message( + this.frm.doc.modified_by, + __("You last edited this", null), + __("{0} last edited this", [get_user_link(this.frm.doc.modified_by)]) + ) + + " · " + + comment_when(this.frm.doc.modified) + ); + this.sidebar + .find(".created-by") + .html( + get_user_message( + this.frm.doc.owner, + __("You created this", null), + __("{0} created this", [get_user_link(this.frm.doc.owner)]) + ) + + " · " + + comment_when(this.frm.doc.creation) + ); } show_auto_repeat_status() { diff --git a/frappe/public/js/frappe/form/tab.js b/frappe/public/js/frappe/form/tab.js index f8bc2368ed..52b2835e47 100644 --- a/frappe/public/js/frappe/form/tab.js +++ b/frappe/public/js/frappe/form/tab.js @@ -1,3 +1,9 @@ +const ICON_MAP = { + "More Info": "info", + Dashboard: "layout-dashboard", + Details: "notepad-text", + Connections: "waypoints", +}; export default class Tab { constructor(layout, df, frm, tab_link_container, tabs_content) { this.layout = layout; @@ -29,6 +35,7 @@ export default class Tab { type="button" role="tab" aria-controls="${id}"> + ${frappe.utils.icon(this.df.icon || ICON_MAP[this.label] || "list")} ${__(this.label, null, this.doctype)} diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index c21a91a2fa..638fdf51d5 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -1,5 +1,5 @@ -