diff --git a/cypress/fixtures/sample_attachments/attachment-1.jpg b/cypress/fixtures/sample_attachments/attachment-1.jpg new file mode 100644 index 0000000000..be6e4f0991 Binary files /dev/null and b/cypress/fixtures/sample_attachments/attachment-1.jpg differ diff --git a/cypress/fixtures/sample_attachments/attachment-10.txt b/cypress/fixtures/sample_attachments/attachment-10.txt new file mode 100644 index 0000000000..9a037142aa --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-10.txt @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-11.txt b/cypress/fixtures/sample_attachments/attachment-11.txt new file mode 100644 index 0000000000..9d607966b7 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-11.txt @@ -0,0 +1 @@ +11 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-2.txt b/cypress/fixtures/sample_attachments/attachment-2.txt new file mode 100644 index 0000000000..d8263ee986 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-2.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-3.txt b/cypress/fixtures/sample_attachments/attachment-3.txt new file mode 100644 index 0000000000..e440e5c842 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-3.txt @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-4.txt b/cypress/fixtures/sample_attachments/attachment-4.txt new file mode 100644 index 0000000000..bf0d87ab1b --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-4.txt @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-5.txt b/cypress/fixtures/sample_attachments/attachment-5.txt new file mode 100644 index 0000000000..7813681f5b --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-5.txt @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-6.txt b/cypress/fixtures/sample_attachments/attachment-6.txt new file mode 100644 index 0000000000..62f9457511 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-6.txt @@ -0,0 +1 @@ +6 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-7.txt b/cypress/fixtures/sample_attachments/attachment-7.txt new file mode 100644 index 0000000000..c7930257df --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-7.txt @@ -0,0 +1 @@ +7 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-8.txt b/cypress/fixtures/sample_attachments/attachment-8.txt new file mode 100644 index 0000000000..301160a930 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-8.txt @@ -0,0 +1 @@ +8 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-9.txt b/cypress/fixtures/sample_attachments/attachment-9.txt new file mode 100644 index 0000000000..f11c82a4cb --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-9.txt @@ -0,0 +1 @@ +9 \ No newline at end of file diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js index 91c38ef6ce..320403bcfa 100644 --- a/cypress/integration/sidebar.js +++ b/cypress/integration/sidebar.js @@ -13,6 +13,27 @@ const verify_attachment_visibility = (document, is_private) => { cy.get_open_dialog().findByRole("checkbox", { name: "Private" }).should(assertion); }; +const attach_file = (file, no_of_files = 1) => { + let files = []; + if (file) { + files = [file]; + } else if (no_of_files > 1) { + // attach n files + files = [...Array(no_of_files)].map( + (el, idx) => + "cypress/fixtures/sample_attachments/attachment-" + + (idx + 1) + + (idx == 0 ? ".jpg" : ".txt") + ); + } + + cy.findByRole("button", { name: "Attach File" }).click(); + cy.get_open_dialog().find(".file-upload-area").selectFile(files, { + action: "drag-drop", + }); + cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); +}; + context("Sidebar", () => { before(() => { cy.visit("/login"); @@ -35,6 +56,36 @@ context("Sidebar", () => { verify_attachment_visibility("blog-post/test-blog-attachment-post", false); }); + it("Verify attachment accessibility UX", () => { + cy.call("frappe.tests.ui_test_helpers.create_todo_with_attachment_limit", { + description: "Sidebar Attachment Access Test ToDo", + }).then((todo) => { + cy.visit(`/app/todo/${todo.message.name}`); + + // explore icon btn should be hidden as there are no attachments + cy.get(".explore-btn").should("be.hidden"); + + attach_file("cypress/fixtures/sample_image.jpg"); + cy.get(".explore-btn").should("be.visible"); + cy.get(".show-all-btn").should("be.hidden"); + + // attach 10 images + attach_file(null, 10); + cy.get(".show-all-btn").should("be.visible"); + + // attach 1 more image to reach attachment limit + attach_file("cypress/fixtures/sample_attachments/attachment-11.txt"); + cy.get(".explore-full-btn").should("be.visible"); + cy.get(".attachments-actions").should("be.hidden"); + cy.get(".explore-btn").should("be.hidden"); + + // test "Show All" button + cy.get(".attachment-row").should("have.length", 10); + cy.get(".show-all-btn").click(); + cy.get(".attachment-row").should("have.length", 12); + }); + }); + it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Sidebar Attachment ToDo", diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js index 052772c54e..35d0e3e51c 100644 --- a/frappe/core/doctype/file/file.js +++ b/frappe/core/doctype/file/file.js @@ -27,8 +27,7 @@ frappe.ui.form.on("File", { preview_file: function (frm) { let $preview = ""; - let file_name = frm.doc.file_name.split("?")[0]; - let file_extension = file_name.split(".").pop()?.toLowerCase(); + let file_extension = frm.doc.file_type.toLowerCase(); if (frappe.utils.is_image_file(frm.doc.file_url)) { $preview = $(`
diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index 01871af5a5..0477d82383 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -8,6 +8,8 @@ "field_order": [ "file_name", "is_private", + "column_break_7jmm", + "file_type", "preview", "preview_html", "section_break_5", @@ -169,13 +171,25 @@ "fieldtype": "Check", "label": "Uploaded To Google Drive", "read_only": 1 + }, + { + "fieldname": "column_break_7jmm", + "fieldtype": "Column Break" + }, + { + "fieldname": "file_type", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "File Type", + "read_only": 1 } ], "force_re_route_to_default_view": 1, "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2023-08-02 09:43:51.178011", + "modified": "2023-08-02 09:43:51.178012", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index d334eaad1e..985e5f50ba 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -44,6 +44,7 @@ class File(Document): content_hash: DF.Data | None file_name: DF.Data | None file_size: DF.Int + file_type: DF.Data | None file_url: DF.Code | None folder: DF.Link | None is_attachments_folder: DF.Check @@ -86,6 +87,7 @@ class File(Document): self.set_folder_name() self.set_file_name() self.validate_attachment_limit() + self.set_file_type() if self.is_folder: return @@ -330,6 +332,17 @@ class File(Document): elif not self.is_home_folder: self.folder = "Home" + def set_file_type(self): + if self.is_folder: + return + + file_type = mimetypes.guess_type(self.file_name)[0] + if not file_type: + return + + file_extension = mimetypes.guess_extension(file_type) + self.file_type = file_extension.lstrip(".").upper() if file_extension else None + def validate_file_on_disk(self): """Validates existence file""" full_path = self.get_full_path() diff --git a/frappe/patches.txt b/frappe/patches.txt index c16bf8d1d7..120a2b07d8 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -228,3 +228,4 @@ execute:frappe.delete_doc_if_exists("Workspace", "Customization") execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter") execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot") frappe.patches.v15_0.move_event_cancelled_to_status +frappe.patches.v15_0.set_file_type diff --git a/frappe/patches/v15_0/set_file_type.py b/frappe/patches/v15_0/set_file_type.py new file mode 100644 index 0000000000..0e9640c14e --- /dev/null +++ b/frappe/patches/v15_0/set_file_type.py @@ -0,0 +1,30 @@ +import mimetypes + +import frappe + + +def execute(): + """Set 'File Type' for all files based on file extension.""" + files = frappe.db.get_all( + "File", + fields=["name", "file_name", "file_url"], + filters={"is_folder": 0, "file_type": ("is", "not set")}, + ) + + frappe.db.auto_commit_on_many_writes = 1 + + for file in files: + file_extension = get_file_extension(file.file_name or file.file_url) + if file_extension: + frappe.db.set_value("File", file.name, "file_type", file_extension, update_modified=False) + + frappe.db.auto_commit_on_many_writes = 0 + + +def get_file_extension(file_name): + file_type = mimetypes.guess_type(file_name)[0] + if not file_type: + return None + + file_extension = mimetypes.guess_extension(file_type) + return file_extension.lstrip(".").upper() if file_extension else None diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index b7763c397d..c8785a97c8 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -1,9 +1,12 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt - frappe.ui.form.Attachments = class Attachments { constructor(opts) { $.extend(this, opts); + + this.attachments_page_length = 10; // show n attachments initially + this.show_all_attachments = false; + this.make(); } make() { @@ -11,7 +14,16 @@ frappe.ui.form.Attachments = class Attachments { this.parent.find(".add-attachment-btn").click(function () { me.new_attachment(); }); - this.add_attachment_wrapper = this.parent.find(".add-attachment-btn"); + + this.parent.find(".explore-btn").click(() => { + frappe.open_in_new_tab = true; + frappe.set_route("List", "File", { + attached_to_doctype: this.frm.doctype, + attached_to_name: this.frm.docname, + }); + }); + + this.add_attachment_wrapper = this.parent.find(".attachments-actions"); this.attachments_label = this.parent.find(".attachments-label"); } max_reached(raise_exception = false) { @@ -31,8 +43,6 @@ frappe.ui.form.Attachments = class Attachments { return false; } refresh() { - var me = this; - if (this.frm.doc.__islocal) { this.parent.toggle(false); return; @@ -42,12 +52,66 @@ frappe.ui.form.Attachments = class Attachments { var max_reached = this.max_reached(); this.add_attachment_wrapper.toggle(!max_reached); + this.setup_expanded_explore_button(max_reached); // add attachment objects var attachments = this.get_attachments(); - if (attachments.length) { + this.render_attachments(attachments); + this.setup_show_all_button(attachments); + } + + setup_expanded_explore_button(max_reached) { + if (!max_reached) { + this.parent.find(".explore-full-btn").addClass("hidden"); + return; + } + + this.parent.find(".explore-full-btn").removeClass("hidden"); + this.parent.find(".explore-full-btn").click(() => { + frappe.set_route("List", "File", { + attached_to_doctype: this.frm.doctype, + attached_to_name: this.frm.docname, + }); + }); + } + + setup_show_all_button(attachments) { + // show button if there is more to show and user has not clicked on "Show All" + let is_slicable = attachments.length > this.attachments_page_length; + let show = !this.show_all_attachments && is_slicable; + + let show_all_btn = this.parent.find(".show-all-btn"); + if (!show) { + show_all_btn.addClass("hidden"); + return; + } + + show_all_btn.removeClass("hidden"); + show_all_btn.click(() => { + show_all_btn.addClass("hidden"); + this.show_all_attachments = true; + this.refresh(); + }); + } + + get_attachments() { + return this.frm.get_docinfo().attachments || []; + } + + render_attachments(attachments) { + var me = this; + let attachments_to_render = attachments; + + let is_slicable = attachments.length > this.attachments_page_length; + if (!this.show_all_attachments && is_slicable) { + // render last n attachments as they are at the top + let start = attachments.length - this.attachments_page_length; + attachments_to_render = attachments.slice(start, attachments.length); + } + + if (attachments_to_render.length) { let exists = {}; - let unique_attachments = attachments.filter((attachment) => { + let unique_attachments = attachments_to_render.filter((attachment) => { return Object.prototype.hasOwnProperty.call(exists, attachment.file_name) ? false : (exists[attachment.file_name] = true); @@ -55,13 +119,15 @@ frappe.ui.form.Attachments = class Attachments { unique_attachments.forEach((attachment) => { me.add_attachment(attachment); }); - } else { + } + + if (!attachments.length) { + // If no attachments in totality this.attachments_label.removeClass("has-attachments"); + this.parent.find(".explore-btn").toggle(false); // hide explore icon button } } - get_attachments() { - return this.frm.get_docinfo().attachments || []; - } + add_attachment(attachment) { var file_name = attachment.file_name; var file_url = this.get_file_url(attachment); @@ -101,8 +167,11 @@ frappe.ui.form.Attachments = class Attachments { $(`
  • `) .append(frappe.get_data_pill(file_label, fileid, remove_action, icon)) - .insertAfter(this.attachments_label.addClass("has-attachments")); + .insertAfter(this.add_attachment_wrapper); + + this.parent.find(".explore-btn").toggle(true); // show explore icon button if hidden } + get_file_url(attachment) { var file_url = attachment.file_url; if (!file_url) { diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index dcea2f4647..edfc194121 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -54,17 +54,43 @@
  • + +
  • + + + +
  • + + + -
  • - -
  • +