├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── codeql.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── frappe_better_attach_control ├── __init__.py ├── api │ ├── __init__.py │ ├── attachment.py │ ├── common.py │ ├── field.py │ ├── file_manager.py │ └── website.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── frappe_better_attach_control │ └── __init__.py ├── hooks.py ├── modules.txt ├── patches.txt ├── public │ ├── build.json │ ├── css │ │ └── better_attach.bundle.css │ └── js │ │ ├── better_attach.bundle.js │ │ ├── controls │ │ ├── attach.js │ │ ├── attach_image.js │ │ ├── v12 │ │ │ ├── attach.js │ │ │ └── attach_image.js │ │ └── v13 │ │ │ ├── attach.js │ │ │ └── attach_image.js │ │ ├── filetypes │ │ └── index.js │ │ ├── uploader │ │ ├── index.js │ │ ├── v12 │ │ │ └── index.js │ │ └── v13 │ │ │ └── index.js │ │ └── utils │ │ └── index.js └── version.py ├── images ├── screenshot_1.png ├── screenshot_2.png └── screenshot_3.png ├── pyproject.toml ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]: " 5 | labels: bug 6 | assignees: kid1194 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQ]: " 5 | labels: enhancement 6 | assignees: kid1194 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main", "v1" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'javascript', 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | - name: Set up Python 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: '3.x' 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | setup-python-dependencies: false 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | # queries: security-extended,security-and-quality 57 | 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v2 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v2 76 | with: 77 | category: "/language:${{matrix.language}}" 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2024 Level Up Marketing & Development Services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies 12 | or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include frappe_better_attach_control *.css 8 | recursive-include frappe_better_attach_control *.csv 9 | recursive-include frappe_better_attach_control *.html 10 | recursive-include frappe_better_attach_control *.ico 11 | recursive-include frappe_better_attach_control *.js 12 | recursive-include frappe_better_attach_control *.json 13 | recursive-include frappe_better_attach_control *.md 14 | recursive-include frappe_better_attach_control *.png 15 | recursive-include frappe_better_attach_control *.py 16 | recursive-include frappe_better_attach_control *.svg 17 | recursive-include frappe_better_attach_control *.txt 18 | recursive-exclude frappe_better_attach_control *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control 2 | 3 | A small plugin for Frappe that adds customization to the attach control. 4 | It supports RTL layout and dark mode out of the box. 5 | 6 | ⚠️ **v2 is still in BETA stage** ⚠️ 7 | 8 | ![v2 Beta17](https://img.shields.io/badge/v2_Beta17-2024/06/13-green?style=plastic) 9 | 10 | **Apologies in advance for any problem or bug you face with this module.** 11 | **Please report any problem or bug you face so it can be fixed.** 12 | 13 | --- 14 | 15 |

16 | Better Attach Control 17 |

18 |

19 | Better Attach Control 20 |

21 |

22 | Better Attach Control 23 |

24 | 25 | --- 26 | 27 | ### Status 28 | - **Desk**: 🔵 Testing 29 | - **Web Form**: 🔵 Testing 30 | 31 | --- 32 | 33 | ### Special Thanks 34 | **A simple display of gratitude and appreciation to those who provided helped and kind support.** 35 | #### Version 2 36 | - [![MohsinAli](https://img.shields.io/badge/MohsinAli-Debug_%7C_Test_%7C_Fix-ff0000?style=plastic)](https://github.com/mohsinalimat) 37 | - [![Robert C](https://img.shields.io/badge/Robert_C-Debug_%7C_Test-orange?style=plastic)](https://github.com/robert1112) 38 | - [![NirajRegmi](https://img.shields.io/badge/NirajRegmi-Debug_%7C_Test-orange?style=plastic)](https://github.com/NirajRegmi) 39 | - [![galaxlabs](https://img.shields.io/badge/galaxlabs-Enhancement-blue?style=plastic)](https://github.com/galaxlabs) 40 | #### Version 1 41 | - [![CA. B.C.Chechani](https://img.shields.io/badge/CA._B.C.Chechani-Debug_%7C_Test-blue?style=plastic)](https://github.com/chechani) 42 | 43 | --- 44 | 45 | ### Table of Contents 46 | - [Requirements](#requirements) 47 | - [Setup](#setup) 48 | - [Install](#install) 49 | - [Update](#update) 50 | - [Uninstall](#uninstall) 51 | - [Usage](#usage) 52 | - [Available Field Options](#available-field-options) 53 | - [Available JavaScript Methods](#available-javascript-methods) 54 | - [Supported Fields](#supported-fields) 55 | - [Issues](#issues) 56 | - [License](#license) 57 | 58 | --- 59 | 60 | ### Requirements 61 | - Frappe >= v12.0.0 62 | 63 | --- 64 | 65 | ### Setup 66 | 67 | ⚠️ **Do not forget to replace [sitename] with the name of your site in all commands.** ⚠️ 68 | 69 | #### Install 70 | 1. Go to bench directory 71 | 72 | ``` 73 | cd ~/frappe-bench 74 | ``` 75 | 76 | 2. Get plugin from Github 77 | 78 | *(Required only once)* 79 | 80 | ``` 81 | bench get-app https://github.com/kid1194/frappe-better-attach-control 82 | ``` 83 | 84 | 3. Build plugin 85 | 86 | *(Required only once)* 87 | 88 | ``` 89 | bench build --app frappe_better_attach_control 90 | ``` 91 | 92 | 4. Install plugin on a specific site 93 | 94 | ``` 95 | bench --site [sitename] install-app frappe_better_attach_control 96 | ``` 97 | 98 | 5. Check the [usage](#usage) section below 99 | 100 | #### Update 101 | 1. Go to app directory 102 | 103 | ``` 104 | cd ~/frappe-bench/apps/frappe_better_attach_control 105 | ``` 106 | 107 | 2. Get updates from Github 108 | 109 | ``` 110 | git pull 111 | ``` 112 | 113 | 3. Go to bench directory 114 | 115 | ``` 116 | cd ~/frappe-bench 117 | ``` 118 | 119 | 4. Build plugin 120 | 121 | ``` 122 | bench build --app frappe_better_attach_control 123 | ``` 124 | 125 | 5. Update a specific site 126 | 127 | ``` 128 | bench --site [sitename] migrate 129 | ``` 130 | 131 | 6. (Optional) Restart bench to clear cache 132 | 133 | ``` 134 | bench restart 135 | ``` 136 | 137 | #### Uninstall 138 | 1. Go to bench directory 139 | 140 | ``` 141 | cd ~/frappe-bench 142 | ``` 143 | 144 | 2. Uninstall plugin from a specific site 145 | 146 | ``` 147 | bench --site [sitename] uninstall-app frappe_better_attach_control 148 | ``` 149 | 150 | 3. Remove plugin from bench 151 | 152 | ``` 153 | bench remove-app frappe_better_attach_control 154 | ``` 155 | 156 | 4. (Optional) Restart bench to clear cache 157 | 158 | ``` 159 | bench restart 160 | ``` 161 | 162 | --- 163 | 164 | ### Usage 165 | 1. Go to Customization > Customize Form 166 | 2. Enter the form doctype (Ex: 'User') 167 | 3. Scroll down to the fields area 168 | 4. Create an **Attach** or **Attach Image** field or edit an existing custom field 169 | 5. Inside the field's **Options** property, add the options you want as a JSON string. 170 | 171 | Ex: ```{"allowed_file_types": [".jpg", ".png", ".gif"]}``` 172 | 173 | ##### ⚠️ Remember 174 | You can't modify the original fields of a doctype, so create a new field or clone and modify the entire doctype. 175 | 176 | --- 177 | 178 | ### Available Field Options 179 | | Option | Description | 180 | | :--- | :--- | 181 | | **dialog_title** | Upload dialog title to be displayed ️(🔶Frappe >= v14.0.0).

🔹Example: ```"Upload Images"```
🔹Default: ```"Upload"``` | 182 | | **upload_notes** | Upload text to be displayed.

🔹Example: ```"Only images and videos, with maximum size of 2MB, are allowed to be uploaded"```
🔹Default: ```""``` | 183 | | **disable_auto_save** | Disable form auto save after upload.

🔹Default: ```false``` | 184 | | **disable_file_browser** | Disable file browser uploads.

⚠️ *(File browser is always disabled in Web Form)*

🔹Default: ```false``` | 185 | | **allow_multiple** | Allow multiple uploads.

⚠️ *(Field value is a JSON array of files url)*

🔹Default: ```false``` | 186 | | **max_file_size** | Maximum file size (in bytes) that is allowed to be uploaded.

🔹Example: ```2048``` for ```2KB```
🔹Default: ```Value of maximum file size in Frappe's settings``` | 187 | | **allowed_file_types** | Array of allowed file types (mimes) or extensions to upload. Prefix escaped RegExp string types with ```$```.

⚠️ *(File extensions must have a leading dot ".")*
⚠️ *(RegExp string types will not be used to in HTML accept attribute)*

🔹Example: ```["image/*", "video/*", ".pdf", ".doc", "$audio\/([a-z]+)"]```
🔹Default: ```null``` or ```["image/*"]``` | 188 | | **max_number_of_files** | Maximum number of files allowed to be uploaded if multiple upload is allowed.

⚠️ *(Bypassing the maximum attachments of doctype might not work)*

🔹Example: ```4```
🔹Default: ```Value of maximum attachments set for the doctype``` | 189 | | **crop_image_aspect_ratio** | Crop aspect ratio for images (🔶Frappe >= v14.0.0).

🔹Example: ```1``` or ```16/9``` or ```4/3```
🔹Default: ```null``` | 190 | | **as_public** | Force uploads to be saved in public folder by default.

🔹Default: ```false``` | 191 | | **allowed_filename** | Only allow files that match a specific file name to be uploaded.

🔹Example: (String)```"picture.png"``` or (RegExp String)```"/picture\-([0-9]+)\.png/"```
🔹Default: ```null``` | 192 | | **allow_reload** | Allow reloading attachments (🔶Frappe >= v13.0.0).

🔶 Affect the visibility of the reload button.🔶

🔹Default: ```true``` | 193 | | **allow_remove** | Allow removing and clearing attachments.

🔶 Affect the visibility of the remove and clear buttons.🔶

🔹Default: ```true``` | 194 | | **users** 🔴 | Array of custom options for a specific user or group of users.

🔹Example: ```[{"for": "Guest", "disabled": true}, {"for": ["Administrator", "user"], "allow_multiple": true}]```
🔹Default: ```null``` | 195 | | **roles** 🔴 | Array of custom options for a specific role or group of roles.
⚠️ *(Custom options for users is prioritized over roles.)*

🔹Example: ```[{"for": ["Administrator", "System"], "allow_multiple": true}]```
🔹Default: ```null``` | 196 | 197 | 🔴 New - 🔵 Changed 198 | 199 | --- 200 | 201 | ### Available JavaScript Methods 202 | | Method | Description | 203 | | :--- | :--- | 204 | | **toggle_auto_save(enable: Boolean !Optional)** 🔵 | Enable/Disable form auto save after upload. | 205 | | **toggle_reload(allow: Boolean !Optional)** | Allow/Deny reloading attachments and toggle the reload button (🔶Frappe >= v13.0.0). | 206 | | **toggle_remove(allow: Boolean !Optional)** | Allow/Deny removing and clearing attachments and toggle the clear and remove buttons. | 207 | | **set_options(options: JSON Object)** | Set or change the plugin options. | 208 | 209 | 🔴 New - 🔵 Changed 210 | 211 | --- 212 | 213 | ### Supported Fields 214 | - Attach 215 | - Attach Image 216 | 217 | --- 218 | 219 | ### Issues 220 | If you find bug in the plugin, please create a [bug report](https://github.com/kid1194/frappe-better-attach-control/issues/new?assignees=kid1194&labels=bug&template=bug_report.md&title=%5BBUG%5D) and let us know about it. 221 | 222 | --- 223 | 224 | ### License 225 | This repository has been released under the [MIT License](https://github.com/kid1194/frappe-better-attach-control/blob/main/LICENSE). -------------------------------------------------------------------------------- /frappe_better_attach_control/__init__.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | __version__ = "2.0.0" -------------------------------------------------------------------------------- /frappe_better_attach_control/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from .attachment import * 8 | from .file_manager import * -------------------------------------------------------------------------------- /frappe_better_attach_control/api/attachment.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | _FILE_DOCTYPE_ = "File" 11 | # For version > 13 12 | _ALLOWED_MIMETYPES_ = ( 13 | "image/png", 14 | "image/jpeg", 15 | "application/pdf", 16 | "application/msword", 17 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 18 | "application/vnd.ms-excel", 19 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 20 | "application/vnd.oasis.opendocument.text", 21 | "application/vnd.oasis.opendocument.spreadsheet", 22 | "text/plain", 23 | ) 24 | 25 | 26 | @frappe.whitelist(allow_guest=True) 27 | def upload_file(): 28 | from frappe_better_attach_control.version import is_version_gt 29 | 30 | user = None 31 | ignore_permissions = False 32 | 33 | if is_version_gt(12): 34 | if frappe.session.user == "Guest": 35 | if frappe.get_system_settings("allow_guests_to_upload_files"): 36 | ignore_permissions = True 37 | else: 38 | raise frappe.PermissionError 39 | else: 40 | user = frappe.get_doc("User", frappe.session.user) 41 | ignore_permissions = False 42 | 43 | files = frappe.request.files 44 | is_private = frappe.form_dict.is_private 45 | doctype = frappe.form_dict.doctype 46 | docname = frappe.form_dict.docname 47 | fieldname = frappe.form_dict.fieldname 48 | file_url = frappe.form_dict.file_url 49 | folder = frappe.form_dict.folder or "Home" 50 | method = frappe.form_dict.method 51 | filename = None 52 | optimize = False 53 | content = None 54 | 55 | if is_version_gt(13): 56 | filename = frappe.form_dict.file_name 57 | optimize = frappe.form_dict.optimize 58 | 59 | if is_version_gt(12): 60 | import mimetypes 61 | 62 | if "file" in files: 63 | file = files["file"] 64 | content = file.stream.read() 65 | filename = file.filename 66 | 67 | if is_version_gt(13): 68 | content_type = mimetypes.guess_type(filename)[0] 69 | if optimize and content_type.startswith("image/"): 70 | args = {"content": content, "content_type": content_type} 71 | if frappe.form_dict.max_width: 72 | args["max_width"] = int(frappe.form_dict.max_width) 73 | if frappe.form_dict.max_height: 74 | args["max_height"] = int(frappe.form_dict.max_height) 75 | 76 | from frappe.utils.image import optimize_image 77 | content = optimize_image(**args) 78 | 79 | frappe.local.uploaded_file = content 80 | frappe.local.uploaded_filename = filename 81 | 82 | if is_version_gt(13): 83 | if not file_url and content is not None and ( 84 | frappe.session.user == "Guest" or (user and not user.has_desk_access()) 85 | ): 86 | filetype = mimetypes.guess_type(filename)[0] 87 | if filetype not in _ALLOWED_MIMETYPES_: 88 | from frappe import _ 89 | 90 | frappe.throw(_("You can only upload JPG, PNG, PDF, TXT or Microsoft documents.")) 91 | 92 | elif is_version_gt(12): 93 | if not file_url and frappe.session.user == "Guest" or (user and not user.has_desk_access()): 94 | filetype = mimetypes.guess_type(filename)[0] 95 | 96 | if method: 97 | method = frappe.get_attr(method) 98 | frappe.is_whitelisted(method) 99 | return method() 100 | else: 101 | from frappe.utils import cint 102 | 103 | ret = frappe.get_doc({ 104 | "doctype": _FILE_DOCTYPE_, 105 | "attached_to_doctype": doctype, 106 | "attached_to_name": docname, 107 | "attached_to_field": fieldname, 108 | "folder": folder, 109 | "file_name": filename, 110 | "file_url": file_url, 111 | "is_private": cint(is_private), 112 | "content": content, 113 | }) 114 | if is_version_gt(12): 115 | ret.save(ignore_permissions=ignore_permissions) 116 | else: 117 | ret.save() 118 | 119 | return ret 120 | 121 | 122 | @frappe.whitelist(methods=["POST"], allow_guest=True) 123 | def remove_files(files): 124 | if files and isinstance(files, str): 125 | from .common import parse_json_if_valid 126 | 127 | files = parse_json_if_valid(files) 128 | 129 | if not files or not isinstance(files, list): 130 | _send_console_log({ 131 | "message": "Invalid files list", 132 | "data": files 133 | }) 134 | return 0 135 | 136 | file_urls = [] 137 | file_names = [] 138 | for file in files: 139 | if file.startswith("http"): 140 | pass 141 | 142 | if file.startswith(("files/", "private/files/")): 143 | file = "/" + file 144 | 145 | if file.startswith(("/files/", "/private/files/")): 146 | file_urls.append(file) 147 | else: 148 | file_names.append(file) 149 | 150 | if not file_urls and not file_names: 151 | _send_console_log({ 152 | "message": "Invalid files path", 153 | "data": files 154 | }) 155 | return 2 156 | 157 | filters = None 158 | or_filters = None 159 | if file_urls: 160 | filters = {"file_url": ["in", file_urls]} 161 | if file_names: 162 | or_filters = {"file_name": ["in", file_names]} 163 | else: 164 | filters = {"file_name": ["in", file_names]} 165 | 166 | names = frappe.get_all( 167 | _FILE_DOCTYPE_, 168 | fields=["name"], 169 | filters=filters, 170 | or_filters=or_filters, 171 | pluck="name" 172 | ) 173 | if names: 174 | for name in names: 175 | frappe.delete_doc(_FILE_DOCTYPE_, name) 176 | 177 | return 1 178 | 179 | _send_console_log({ 180 | "message": "Files not found", 181 | "data": files 182 | }) 183 | return 3 184 | 185 | 186 | # [Internal] 187 | def _send_console_log(data): 188 | from .common import send_console_log 189 | 190 | send_console_log(data) -------------------------------------------------------------------------------- /frappe_better_attach_control/api/common.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import json 8 | 9 | import frappe 10 | from frappe import _, _dict 11 | 12 | 13 | def error(msg, throw=True): 14 | from frappe_better_attach_control.version import is_version_lt 15 | 16 | title = "Better Attach Control" 17 | if is_version_lt(14): 18 | frappe.log_error(msg, title) 19 | else: 20 | frappe.log_error(title, msg) 21 | if throw: 22 | frappe.throw(msg, title=title) 23 | 24 | 25 | def get_cached_value(dt, filters, field, as_dict=False): 26 | _as_dict = as_dict 27 | 28 | if isinstance(filters, str): 29 | if as_dict and isinstance(field, str): 30 | as_dict = False 31 | 32 | val = frappe.get_cached_value(dt, filters, field, as_dict=as_dict) 33 | if val and isinstance(val, list) and not isinstance(field, list): 34 | val = val.pop() 35 | else: 36 | val = frappe.db.get_value(dt, filters, field, as_dict=as_dict) 37 | 38 | if not val: 39 | error(_("Unable to get the value or values of {0} from {1}, filtered by {2}").format( 40 | to_json_if_valid(field), 41 | dt, 42 | to_json_if_valid( 43 | filters.keys() if isinstance(filters, dict) else filters 44 | ) 45 | )) 46 | 47 | if _as_dict and not isinstance(val, dict): 48 | if isinstance(field, list) and isinstance(val, list): 49 | val = _dict(zip(field, val)) 50 | elif isinstance(field, str): 51 | val = _dict(zip([field], [val])) 52 | 53 | return val 54 | 55 | 56 | def to_json_if_valid(data, default=None): 57 | if not data: 58 | return data 59 | 60 | if default is None: 61 | default = data 62 | try: 63 | return json.dumps(data) 64 | except Exception: 65 | return default 66 | 67 | 68 | def parse_json_if_valid(data, default=None): 69 | if not data: 70 | return data 71 | 72 | if default is None: 73 | default = data 74 | try: 75 | return json.loads(data) 76 | except Exception: 77 | return default 78 | 79 | 80 | def send_console_log(data): 81 | frappe.publish_realtime( 82 | event="better_attach_console", 83 | message=data, 84 | after_commit=True 85 | ) -------------------------------------------------------------------------------- /frappe_better_attach_control/api/field.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | @frappe.whitelist(methods=["POST"], allow_guest=True) 11 | def get_options(doctype, name, webform): 12 | if not doctype or not isinstance(doctype, str): 13 | _send_console_log({ 14 | "message": "Empty or invalid field doctype", 15 | "data": [doctype, name] 16 | }) 17 | return "" 18 | 19 | if not name or not isinstance(name, str): 20 | _send_console_log({ 21 | "message": "Empty or invalid field name", 22 | "data": [doctype, name] 23 | }) 24 | return "" 25 | 26 | fieldtypes = ["in", ["Attach", "Attach Image"]] 27 | options = None 28 | 29 | if webform: 30 | options = frappe.db.get_value( 31 | "Web Form Field", 32 | { 33 | "fieldname": name, 34 | "parent": doctype, 35 | "parenttype": "Web Form", 36 | "parentfield": "web_form_fields", 37 | "fieldtype": fieldtypes 38 | }, 39 | "options" 40 | ) 41 | 42 | else: 43 | options = frappe.db.get_value( 44 | "DocField", 45 | { 46 | "fieldname": name, 47 | "parent": doctype, 48 | "parenttype": "DocType", 49 | "parentfield": "fields", 50 | "fieldtype": fieldtypes 51 | }, 52 | "options" 53 | ) 54 | if not options or not isinstance(options, str): 55 | options = frappe.db.get_value( 56 | "Custom Field", 57 | { 58 | "fieldname": name, 59 | "dt": doctype, 60 | "fieldtype": fieldtypes 61 | }, 62 | "options" 63 | ) 64 | 65 | if options and isinstance(options, str): 66 | return options 67 | 68 | _send_console_log({ 69 | "message": "Empty or invalid field options", 70 | "data": [doctype, name, options] 71 | }) 72 | return "" 73 | 74 | 75 | # [Internal] 76 | def _send_console_log(data): 77 | from .common import send_console_log 78 | 79 | send_console_log(data) -------------------------------------------------------------------------------- /frappe_better_attach_control/api/file_manager.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import os 8 | 9 | import frappe 10 | from frappe import _ 11 | from frappe.utils import cint, cstr, get_url 12 | from frappe.core.doctype.file.file import URL_PREFIXES 13 | 14 | 15 | _FILE_DOCTYPE_ = "File" 16 | _FILE_FIELDS_ = [ 17 | "name", "file_name", "file_url", "is_folder", 18 | "modified", "is_private", "file_size" 19 | ] 20 | 21 | 22 | @frappe.whitelist() 23 | def get_files_in_folder(folder, start=0, page_length=20): 24 | result = _get_files_in_folder(folder, start, page_length) 25 | result["files"] = _prepare_files(result["files"]) 26 | return result 27 | 28 | 29 | @frappe.whitelist() 30 | def get_files_by_search_text(text): 31 | files = _get_files_by_search_text(text) 32 | files = _prepare_files(files) 33 | return files 34 | 35 | 36 | def _get_files_in_folder(folder, start, page_length): 37 | start = cint(start) 38 | page_length = cint(page_length) 39 | files = frappe.get_all( 40 | _FILE_DOCTYPE_, 41 | fields=_FILE_FIELDS_, 42 | filters={"folder": folder}, 43 | start=start, 44 | page_length=page_length + 1 45 | ) 46 | 47 | if folder == "Home": 48 | from .common import get_cached_value 49 | 50 | attachment_folder = get_cached_value( 51 | _FILE_DOCTYPE_, 52 | "Home/Attachments", 53 | _FILE_FIELDS_, 54 | as_dict=1 55 | ) 56 | if attachment_folder not in files: 57 | files.insert(0, attachment_folder) 58 | 59 | return { 60 | "files": files[:page_length], 61 | "has_more": len(files) > page_length, 62 | } 63 | 64 | 65 | def _get_files_by_search_text(text): 66 | if not text: 67 | return [] 68 | 69 | text = "%" + cstr(text).lower() + "%" 70 | return frappe.get_all( 71 | _FILE_DOCTYPE_, 72 | fields=_FILE_FIELDS_, 73 | filters={"is_folder": False}, 74 | or_filters={ 75 | "name": ["like", text], 76 | "file_name": ["like", text], 77 | "file_url": ["like", text], 78 | }, 79 | order_by="modified desc", 80 | limit=20, 81 | ) 82 | 83 | 84 | def _prepare_files(files): 85 | for i in range(len(files)): 86 | file = files[i] 87 | file["size"] = 0 88 | if not cint(file["is_folder"]): 89 | from frappe.utils import flt 90 | 91 | file["size"] = flt(file["file_size"]) 92 | if not file["size"]: 93 | try: 94 | file["size"] = os.path.getsize(_get_full_path(file)) 95 | except Exception: 96 | file["size"] = 0 97 | 98 | del file["is_private"] 99 | del file["file_size"] 100 | files[i] = file 101 | 102 | return files 103 | 104 | 105 | def _get_full_path(file): 106 | file_path = file["file_url"] or file["file_name"] 107 | site_url = get_url() 108 | if "/files/" in file_path and file_path.startswith(site_url): 109 | file_path = file_path.split(site_url, 1)[1] 110 | 111 | if "/" not in file_path: 112 | if file["is_private"]: 113 | file_path = f"/private/files/{file_path}" 114 | else: 115 | file_path = f"/files/{file_path}" 116 | 117 | if file_path.startswith("/private/files/"): 118 | from frappe.utils import get_files_path 119 | 120 | file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1) 121 | 122 | elif file_path.startswith("/files/"): 123 | from frappe.utils import get_files_path 124 | 125 | file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/")) 126 | 127 | elif file_path.startswith(URL_PREFIXES): 128 | pass 129 | 130 | elif not file["file_url"]: 131 | _error(_("There is some problem with the file url: {0}").format(file_path)) 132 | 133 | from frappe.utils.file_manager import is_safe_path 134 | 135 | if not is_safe_path(file_path): 136 | _error(_("Cannot access file path {0}").format(file_path)) 137 | 138 | if os.path.sep in file["file_name"]: 139 | _error(_("File name cannot have {0}").format(os.path.sep)) 140 | 141 | return file_path 142 | 143 | 144 | # [Internal] 145 | def _error(msg): 146 | from .common import error 147 | 148 | error(msg) -------------------------------------------------------------------------------- /frappe_better_attach_control/api/website.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import os 8 | 9 | import frappe 10 | from frappe import _ 11 | from frappe.utils import cstr 12 | 13 | 14 | def website_context(context): 15 | if ( 16 | not context.get("doc", "") or 17 | "doctype" not in context.doc or 18 | "name" not in context.doc or 19 | context.doc.doctype != "Web Form" or 20 | not context.doc.name 21 | ): 22 | return 0 23 | 24 | try: 25 | fields = frappe.get_all( 26 | "Web Form Field", 27 | fields=["fieldname", "options"], 28 | filters={ 29 | "parent": context.doc.name, 30 | "parenttype": "Web Form", 31 | "parentfield": "web_form_fields", 32 | "fieldtype": ["in", ["Attach", "Attach Image"]], 33 | } 34 | ) 35 | if not fields or not isinstance(fields, list): 36 | return 0 37 | 38 | options = {} 39 | for v in fields: 40 | v["options"] = cstr(v["options"]).strip() 41 | if ( 42 | v["options"] and 43 | v["options"].startswith("{") and 44 | v["options"].endswith("{") 45 | ): 46 | options[v["fieldname"]] = v["options"] 47 | 48 | if not options: 49 | return 0 50 | 51 | from .common import to_json_if_valid 52 | 53 | options = to_json_if_valid(options, "") 54 | if not options: 55 | return 0 56 | 57 | script = "frappe.provide('frappe.BAC');" 58 | script = f"{script}\nfrappe.BAC.webform = true;" 59 | script = f"{script}\nfrappe.BAC.options = {options};" 60 | set_context(context, "script", script) 61 | except Exception: 62 | _error(_("Unable to get the Attach fields of the web form.")) 63 | return 0 64 | 65 | app_name = "frappe_better_attach_control" 66 | bundled_js = get_bundled_file_path(app_name, "js") 67 | js_loaded = False 68 | if has_file(bundled_js): 69 | try: 70 | scripts = read_file(bundled_js) 71 | js = bundled_js.strip("/").split("/")[-1] 72 | scripts = f"// {js}\n{scripts}" 73 | set_context(context, "script", scripts) 74 | js_loaded = True 75 | except Exception: 76 | _error(_("Unable to inject the js bundled file to context.")) 77 | 78 | if not js_loaded: 79 | try: 80 | js_files = frappe.get_hooks("better_webform_include_js", default=None, app_name=app_name) 81 | if not js_files: 82 | _error(_("Unable to inject the js files to context.")) 83 | else: 84 | if not isinstance(js_files, list): 85 | js_files = [js_files] 86 | 87 | scripts = [] 88 | for js in js_files: 89 | path = get_file_path(app_name, js) 90 | if has_file(path): 91 | data = read_file(path) 92 | scripts.append(f"// {js}\n{data}") 93 | else: 94 | _error(_("Unable to inject the js file \"{0}\" to context.").format(js)) 95 | 96 | if scripts: 97 | scripts = clean_js_script("\n\n\n".join(scripts)) 98 | scripts = "(function() {\n\n" + scripts + "\n\n}());" 99 | write_file(bundled_js, scripts) 100 | set_context(context, "script", scripts) 101 | except Exception: 102 | _error(_("Unable to inject the js files to context.")) 103 | 104 | try: 105 | css_files = frappe.get_hooks("better_webform_include_css", default=None, app_name=app_name) 106 | if not css_files: 107 | _error(_("Unable to inject the css files to context.")) 108 | else: 109 | if not isinstance(css_files, list): 110 | css_files = [css_files] 111 | 112 | styles = [] 113 | for css in css_files: 114 | path = get_file_path(app_name, css) 115 | if has_file(path): 116 | data = read_file(path) 117 | styles.append(f"// {css}\n{data}") 118 | else: 119 | _error(_("Unable to inject the css file \"{0}\" to context.").format(css)) 120 | 121 | if styles: 122 | set_context(context, "style", "\n\n\n".join(styles)) 123 | except Exception: 124 | _error(_("Unable to inject the css files to context.")) 125 | 126 | 127 | def set_context(context, key, data): 128 | value = context.get(key, "") 129 | if value: 130 | context[key] = "\n\n\n".join([value, data]) 131 | else: 132 | context[key] = data 133 | 134 | 135 | def get_bundled_file_path(app, ext): 136 | from .common import __frappe_base_ver__ as version 137 | 138 | filename = f"better_attach_v{version}.bundle.{ext}" 139 | path = f"/assets/frappe_better_attach_control/{ext}/{filename}" 140 | return get_file_path(app, path) 141 | 142 | 143 | def get_file_path(app, path): 144 | return frappe.get_app_path(app, *path.strip("/").split("/")) 145 | 146 | 147 | def has_file(app, path): 148 | if os.path.exists(path): 149 | return True 150 | 151 | return False 152 | 153 | 154 | def read_file(path): 155 | tmp = open(path) 156 | data = tmp.read() 157 | tmp.close() 158 | return data 159 | 160 | 161 | def write_file(path, data): 162 | tmp = open(path, "w") 163 | tmp.write(data) 164 | tmp.close() 165 | 166 | 167 | def clean_js_script(data): 168 | import re 169 | 170 | rgx = [ 171 | "import([\s\n\r]+|)(.*?)([\s\n\r]+|)from([\s\n\r]+|)(.*?)([\s\n\r]+|)\;", 172 | "import([\s\n\r]+|)(.*?)([\s\n\r]+|)\;", 173 | "import([\s\n\r]+|)\{([\s\n\r]+|)*(([\s\n\r]+|)(.*?)([\s\n\r]+|))*([\s\n\r]+|)\}([\s\n\r]+|)from([\s\n\r]+|)(.*?)([\s\n\r]+|)\;", 174 | "export([\s\n\r]+|)default([\s\n\r]+|)(.*?)([\s\n\r]+|)\;", 175 | "export([\s\n\r]+|)" 176 | ] 177 | data = re.sub("(" + "|".join(rgx) + ")", "", data) 178 | data = re.sub("^([\s\n\r]+)", "", data) 179 | data = re.sub("([\n\r]{3,})", "\n\n\n", data) 180 | return data 181 | 182 | 183 | def _error(msg): 184 | from .common import error 185 | 186 | error(msg, throw=False) -------------------------------------------------------------------------------- /frappe_better_attach_control/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /frappe_better_attach_control/config/desktop.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import _ 8 | 9 | 10 | def get_data(): 11 | return [ 12 | { 13 | "module_name": "Frappe Better Attach Control", 14 | "color": "blue", 15 | "icon": "octicon octicon-paperclip", 16 | "type": "module", 17 | "label": _("Frappe Better Attach Control") 18 | } 19 | ] -------------------------------------------------------------------------------- /frappe_better_attach_control/config/docs.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | """ 8 | Configuration for docs 9 | """ 10 | 11 | 12 | def get_context(context): 13 | context.brand_html = "Frappe Better Attach Control" 14 | -------------------------------------------------------------------------------- /frappe_better_attach_control/frappe_better_attach_control/__init__.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /frappe_better_attach_control/hooks.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from .version import is_version_gt 8 | 9 | 10 | app_name = "frappe_better_attach_control" 11 | app_title = "Frappe Better Attach Control" 12 | app_publisher = "Ameen Ahmed (Level Up)" 13 | app_description = "Frappe attach control that supports customization." 14 | app_icon = "octicon octicon-paperclip" 15 | app_color = "blue" 16 | app_email = "kid1194@gmail.com" 17 | app_license = "MIT" 18 | 19 | 20 | app_include_css = [ 21 | "better_attach.bundle.css" 22 | ] if is_version_gt(13) else [ 23 | "/assets/frappe_better_attach_control/css/better_attach.css" 24 | ] 25 | 26 | 27 | better_webform_include_css = [ 28 | "/assets/frappe_better_attach_control/css/better_attach.bundle.css" 29 | ] 30 | 31 | 32 | app_include_js = [ 33 | "better_attach.bundle.js" 34 | ] if is_version_gt(13) else ([ 35 | "/assets/frappe_better_attach_control/js/better_attach_v13.bundle.js" 36 | ] if is_version_gt(12) else [ 37 | "/assets/frappe_better_attach_control/js/better_attach_v12.bundle.js" 38 | ]) 39 | 40 | 41 | better_webform_include_js = [ 42 | "/assets/frappe_better_attach_control/js/utils/index.js", 43 | "/assets/frappe_better_attach_control/js/filetypes/index.js", 44 | "/assets/frappe_better_attach_control/js/uploader/index.js", 45 | "/assets/frappe_better_attach_control/js/controls/attach.js", 46 | "/assets/frappe_better_attach_control/js/controls/attach_image.js" 47 | ] if is_version_gt(13) else ([ 48 | "/assets/frappe_better_attach_control/js/utils/index.js", 49 | "/assets/frappe_better_attach_control/js/filetypes/index.js", 50 | "/assets/frappe_better_attach_control/js/uploader/v13/index.js", 51 | "/assets/frappe_better_attach_control/js/controls/v13/attach.js", 52 | "/assets/frappe_better_attach_control/js/controls/v13/attach_image.js" 53 | ] if is_version_gt(12) else [ 54 | "/assets/frappe_better_attach_control/js/utils/index.js", 55 | "/assets/frappe_better_attach_control/js/filetypes/index.js", 56 | "/assets/frappe_better_attach_control/js/uploader/v12/index.js", 57 | "/assets/frappe_better_attach_control/js/controls/v12/attach.js", 58 | "/assets/frappe_better_attach_control/js/controls/v12/attach_image.js" 59 | ]) 60 | 61 | 62 | update_website_context = "frappe_better_attach_control.api.website.website_context" -------------------------------------------------------------------------------- /frappe_better_attach_control/modules.txt: -------------------------------------------------------------------------------- 1 | Frappe Better Attach Control -------------------------------------------------------------------------------- /frappe_better_attach_control/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kid1194/frappe-better-attach-control/63e3cdbdddac89b33cc7c101efe717d698a72140/frappe_better_attach_control/patches.txt -------------------------------------------------------------------------------- /frappe_better_attach_control/public/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "frappe_better_attach_control/js/better_attach_v13.bundle.js": [ 3 | "public/js/utils/index.js", 4 | "public/js/filetypes/index.js", 5 | "public/js/uploader/v13/index.js", 6 | "public/js/controls/v13/attach.js", 7 | "public/js/controls/v13/attach_image.js" 8 | ], 9 | "frappe_better_attach_control/js/better_attach_v12.bundle.js": [ 10 | "public/js/utils/index.js", 11 | "public/js/filetypes/index.js", 12 | "public/js/uploader/v12/index.js", 13 | "public/js/controls/v12/attach.js", 14 | "public/js/controls/v12/attach_image.js" 15 | ], 16 | "frappe_better_attach_control/css/better_attach.css": [ 17 | "public/css/better_attach.bundle.css" 18 | ] 19 | } -------------------------------------------------------------------------------- /frappe_better_attach_control/public/css/better_attach.bundle.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | .ba-hidden { 10 | display: none !important; 11 | } 12 | .ba-hidden-overflow { 13 | overflow: hidden; 14 | } 15 | .ba-attachment { 16 | padding-top: 4px !important; 17 | padding-bottom: 4px !important; 18 | } 19 | .ba-link, .ba-link:hover { 20 | text-decoration: none; 21 | } 22 | .ba-link { 23 | max-width: 100%; 24 | white-space: nowrap; 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | vertical-align: middle; 28 | color: #000; 29 | font-size: .8rem; 30 | } 31 | [data-theme="dark"] .ba-link { 32 | color: #fff; 33 | } 34 | .ba-meta { 35 | margin-left: 5px; 36 | margin-right: 0; 37 | padding: 0; 38 | color: #6c757d; 39 | font-size: .6rem; 40 | } 41 | [data-theme="dark"] .ba-meta { 42 | color: #fff; 43 | } 44 | [data-theme="rtl"] .ba-meta { 45 | margin-left: 0; 46 | margin-right: 5px; 47 | } 48 | .ba-remove { 49 | padding: 2px; 50 | } 51 | .ba-file { 52 | width: 15px !important; 53 | height: 15px !important; 54 | margin-left: 0; 55 | margin-right: 5px; 56 | background-position: center center; 57 | background-repeat: no-repeat; 58 | background-size: contain; 59 | } 60 | html[dir="rtl"] .ba-file { 61 | margin-left: 5px; 62 | margin-right: 0; 63 | } 64 | .ba-image { 65 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%2324c824" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM128 256c0 17.7-14.3 32-32 32s-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32zm88 32c5.3 0 10.2 2.6 13.2 6.9l88 128c3.4 4.9 3.7 11.3 1 16.5S310 448 304 448H80c-5.8 0-11.1-3.1-13.9-8.1s-2.8-11.2.2-16.1l48-80c2.9-4.8 8.1-7.8 13.7-7.8s10.8 2.9 13.7 7.8l12.8 21.4l48.3-70.2c3-4.3 7.9-6.9 13.2-6.9z"%2F%3E%3C%2Fsvg%3E'); 66 | } 67 | .ba-word { 68 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%2300a4ef" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM111 257.1l26.8 89.2l31.6-90.3c3.4-9.6 12.5-16.1 22.7-16.1s19.3 6.4 22.7 16.1l31.6 90.3l26.6-89.2c3.8-12.7 17.2-19.9 29.9-16.1s19.9 17.2 16.1 29.9l-48 160c-3 10-12.1 16.9-22.4 17.1s-19.8-6.2-23.2-16.1L192 336.6l-33.3 95.3c-3.4 9.8-12.8 16.3-23.2 16.1s-19.5-7.1-22.4-17.1l-48-160c-3.8-12.7 3.4-26.1 16.1-29.9s26.1 3.4 29.9 16.1z"%2F%3E%3C%2Fsvg%3E'); 69 | } 70 | .ba-presentation { 71 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%23d04423" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM136 240h68c42 0 76 34 76 76s-34 76-76 76h-44v32c0 13.3-10.7 24-24 24s-24-10.7-24-24V264c0-13.3 10.7-24 24-24zm68 104c15.5 0 28-12.5 28-28s-12.5-28-28-28h-44v56h44z"%2F%3E%3C%2Fsvg%3E'); 72 | } 73 | .ba-spreadsheet { 74 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%231d6f42" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM155.7 250.2l36.3 51.9l36.3-51.9c7.6-10.9 22.6-13.5 33.4-5.9s13.5 22.6 5.9 33.4L221.3 344l46.4 66.2c7.6 10.9 5 25.8-5.9 33.4s-25.8 5-33.4-5.9L192 385.8l-36.3 51.9c-7.6 10.9-22.6 13.5-33.4 5.9s-13.5-22.6-5.9-33.4l46.3-66.2l-46.4-66.2c-7.6-10.9-5-25.8 5.9-33.4s25.8-5 33.4 5.9z"%2F%3E%3C%2Fsvg%3E'); 75 | } 76 | .ba-pdf { 77 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%23f40f02" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM64 224h24c30.9 0 56 25.1 56 56s-25.1 56-56 56h-8v32c0 8.8-7.2 16-16 16s-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm24 80c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8v48h8zm72-64c0-8.8 7.2-16 16-16h24c26.5 0 48 21.5 48 48v64c0 26.5-21.5 48-48 48h-24c-8.8 0-16-7.2-16-16V240zm32 112h8c8.8 0 16-7.2 16-16v-64c0-8.8-7.2-16-16-16h-8v96zm96-128h48c8.8 0 16 7.2 16 16s-7.2 16-16 16h-32v32h32c8.8 0 16 7.2 16 16s-7.2 16-16 16h-32v48c0 8.8-7.2 16-16 16s-16-7.2-16-16V240c0-8.8 7.2-16 16-16z"%2F%3E%3C%2Fsvg%3E'); 78 | } 79 | .ba-audio { 80 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%2390caf9" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22c28-16.8 46.6-47.4 46.6-82.3s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8v128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376H80c-8.8 0-16-7.2-16-16v-48c0-8.8 7.2-16 16-16h33.4l35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3c10.1 11.2 16.3 26.2 16.3 42.6s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"%2F%3E%3C%2Fsvg%3E'); 81 | } 82 | .ba-video { 83 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%231976d2" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM64 288c0-17.7 14.3-32 32-32h96c17.7 0 32 14.3 32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32v-96zm236.9 109.9L256 368v-64l44.9-29.9c2-1.3 4.4-2.1 6.8-2.1c6.8 0 12.3 5.5 12.3 12.3v103.4c0 6.8-5.5 12.3-12.3 12.3c-2.4 0-4.8-.7-6.8-2.1z"%2F%3E%3C%2Fsvg%3E'); 84 | } 85 | .ba-compressed { 86 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%23ebd115" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM96 48c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16s-7.2 16-16 16h-32c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16s-7.2 16-16 16h-32c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16s-7.2 16-16 16h-32c-8.8 0-16-7.2-16-16zm-6.3 71.8c3.7-14 16.4-23.8 30.9-23.8h14.8c14.5 0 27.2 9.7 30.9 23.8l23.5 88.2c1.4 5.4 2.1 10.9 2.1 16.4c0 35.2-28.8 63.7-64 63.7s-64-28.5-64-63.7c0-5.5.7-11.1 2.1-16.4l23.5-88.2zM112 336c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16h-32z"%2F%3E%3C%2Fsvg%3E'); 87 | } 88 | .ba-text { 89 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%23a9a9a9" d="M64 0C28.7 0 0 28.7 0 64v384c0 35.3 28.7 64 64 64h256c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zm192 0v128h128L256 0zM112 256h160c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64h160c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64h160c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16z"%2F%3E%3C%2Fsvg%3E'); 90 | } 91 | .ba-other { 92 | background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="0.75em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 384 512"%3E%3Cpath fill="%23778899" d="M0 64C0 28.7 28.7 0 64 0h160v128c0 17.7 14.3 32 32 32h128v288c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0l128 128z"%2F%3E%3C%2Fsvg%3E'); 93 | } -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/better_attach.bundle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import './uploader'; 10 | import './controls/attach.js'; 11 | import './controls/attach_image.js'; -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/controls/attach.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import Helpers from './../utils'; 10 | import Filetype from './../filetypes'; 11 | 12 | 13 | frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.ControlAttach { 14 | make() { 15 | super.make(); 16 | this._setup_control(); 17 | } 18 | make_input() { 19 | this._update_options(); 20 | super.make_input(); 21 | this._toggle_remove_button(); 22 | this._setup_display(); 23 | } 24 | clear_attachment() { 25 | if (!this._allow_remove) return; 26 | if (!this.frm) { 27 | if (!this._value.length) this._reset_input(); 28 | else this._remove_files(this._value, function(ret) { 29 | if (cint(ret)) this._reset_input(); 30 | else Helpers.error('Unable to clear the uploaded attachments.'); 31 | }); 32 | return; 33 | } 34 | // To prevent changing value from within set_input function 35 | this._prevent_input = true; 36 | this.parse_validate_and_set_in_model(null); 37 | this.refresh(); 38 | if (!this._value.length) { 39 | this._reset_value(); 40 | this.refresh(); 41 | this._form_save(); 42 | // To allow changing value from within set_input function 43 | this._prevent_input = false; 44 | return; 45 | } 46 | this._remove_files(this._value, function(ret) { 47 | if (!cint(ret)) return Helpers.error('Unable to clear the uploaded attachments.'); 48 | if (this.frm.attachments) 49 | Helpers.each(this._value, function(v) { 50 | let fid = this.frm.attachments.get_file_id_from_file_url(v); 51 | fid && this.frm.attachments.remove_fileid(fid); 52 | }, this); 53 | 54 | this.frm.sidebar && this.frm.sidebar.reload_docinfo(); 55 | this.parse_validate_and_set_in_model(null) 56 | .then(Helpers.fnBind(function() { 57 | this._reset_value(); 58 | this.refresh(); 59 | this._form_save(); 60 | // To allow changing value from within set_input function 61 | this._prevent_input = false; 62 | }, this)) 63 | .catch(Helpers.fnBind(function() { 64 | // To allow changing value from within set_input function before failure 65 | this._prevent_input = false; 66 | }, this)); 67 | }, function() { 68 | // To allow changing value from within set_input function before failure 69 | this._prevent_input = false; 70 | }); 71 | } 72 | reload_attachment() { 73 | this._allow_reload && super.reload_attachment(); 74 | } 75 | on_attach_click() { 76 | if (this._images_only) this.on_attach_doc_image(); 77 | else super.on_attach_click(); 78 | } 79 | on_attach_doc_image() { 80 | this.set_upload_options(); 81 | this._set_image_upload_options(); 82 | this.file_uploader = new frappe.ui.FileUploader(this.image_upload_options); 83 | } 84 | set_upload_options() { 85 | if (this.upload_options) return; 86 | this._update_options(); 87 | var opts = this._options && this.df.options; 88 | if (opts) this.df.options = this._options; 89 | super.set_upload_options(); 90 | if (opts) this.df.options = opts; 91 | } 92 | set_value(value, force_set_value=false) { 93 | // Prevent changing value if called from event 94 | if (this._prevent_input) return Promise.resolve(); 95 | value = this._set_value(value); 96 | if (!this.frm) this._updating_input = true; 97 | return super.set_value(value, force_set_value); 98 | } 99 | set_input(value, dataurl) { 100 | // Prevent changing value if called from event 101 | if (this._prevent_input) return; 102 | if (this._updating_input) { 103 | this._updating_input = false; 104 | if (this._value.length) this._update_input(); 105 | return; 106 | } 107 | if (value === null) { 108 | if (!this._value.length) this._reset_value(); 109 | else this._remove_files(this._value, function(ret) { 110 | if (cint(ret)) this._reset_value(); 111 | else Helpers.error('Unable to delete the uploaded attachments.'); 112 | }); 113 | return; 114 | } 115 | if (Helpers.isEmpty(value)) return; 116 | let val = Helpers.toArray(value, null); 117 | if (Helpers.isArray(val)) { 118 | if (!val.length) return; 119 | var update = 0; 120 | if (!this._allow_multiple) { 121 | value = val[0]; 122 | if (Helpers.isString(value) && this._value.indexOf(value) < 0) { 123 | this._set_value(value); 124 | update = 1; 125 | } 126 | } else { 127 | this._multiple_values = true; 128 | Helpers.each(val, function(v) { 129 | if (Helpers.isString(v) && this._value.indexOf(value) < 0) { 130 | this._set_value(v); 131 | update = 1; 132 | } 133 | }, this); 134 | } 135 | if (update) this._update_input(); 136 | this._multiple_values = false; 137 | this._process_files(); 138 | return; 139 | } 140 | if (!Helpers.isString(value)) return; 141 | this.value = this._set_value(value); 142 | this._update_input(value, dataurl); 143 | } 144 | async on_upload_complete(attachment) { 145 | if (this.frm) { 146 | await this.parse_validate_and_set_in_model(attachment.file_url); 147 | this.frm.attachments && this.frm.attachments.update_attachment(attachment); 148 | if (!this._allow_multiple) this._form_save(); 149 | else { 150 | let up = this.file_uploader && this.file_uploader.uploader; 151 | if (up && up.files && up.files.every(function(file) { 152 | return !file.failed && file.request_succeeded; 153 | })) this._form_save(); 154 | } 155 | } 156 | this.set_value(attachment.file_url); 157 | } 158 | toggle_reload_button() { 159 | if (!this.$value) return; 160 | let show = this._allow_reload && this.file_uploader 161 | && this.file_uploader.uploader.files 162 | && this.file_uploader.uploader.files.length > 0; 163 | this.$value.find('[data-action="reload_attachment"]').toggle(show); 164 | } 165 | refresh() { 166 | super.refresh(); 167 | if (Helpers.isString(this.df.options)) 168 | this.df.options = Helpers.parseJson(this.df.options, {}); 169 | else if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; 170 | if (!Helpers.isEqual(this.df.options, this._ls_options)) 171 | this._update_options(true); 172 | } 173 | // Custom Methods 174 | toggle_auto_save(enable) { 175 | if (enable != null) this._disable_auto_save = enable ? false : true; 176 | else this._disable_auto_save = !this._disable_auto_save; 177 | } 178 | toggle_reload(allow) { 179 | if (allow != null) this._allow_reload = !!allow; 180 | else this._allow_reload = !this._allow_reload; 181 | this.toggle_reload_button(); 182 | } 183 | toggle_remove(allow) { 184 | if (allow != null) this._allow_remove = !!allow; 185 | else this._allow_remove = !this._allow_remove; 186 | this._toggle_remove_button(); 187 | } 188 | set_options(opts) { 189 | if (Helpers.isString(opts) && opts.length) opts = Helpers.parseJson(opts, null); 190 | if (Helpers.isEmpty(opts) || !Helpers.isPlainObject(opts)) return; 191 | opts = Helpers.merge(this.df.options, opts); 192 | if (Helpers.isEqual(this.df.options, opts)) return; 193 | this.df.options = opts; 194 | this._update_options(true); 195 | } 196 | // Private Methods 197 | _setup_control() { 198 | this._doctype = (this.frm && this.frm.doctype) 199 | || this.doctype 200 | || (this.doc && this.doc.doctype) 201 | || null; 202 | this._is_webform = (frappe.BAC && !!frappe.BAC.webform) 203 | || this._doctype === 'Web Form' 204 | || this.df.parenttype === 'Web Form' 205 | || this.df.is_web_form 206 | || (this.doc && this.doc.web_form_name); 207 | 208 | if (this._is_webform && frappe.BAC) { 209 | if (Helpers.isString(frappe.BAC.options)) 210 | frappe.BAC.options = Helpers.parseJson(frappe.BAC.options, {}); 211 | if (!Helpers.isPlainObject(frappe.BAC.options)) frappe.BAC.options = {}; 212 | if (frappe.BAC.options[this.df.fieldname]) 213 | this.df.options = frappe.BAC.options[this.df.fieldname]; 214 | } 215 | 216 | this._ls_options = null; 217 | this._options = null; 218 | this._value = []; 219 | this._files = []; 220 | this._disable_auto_save = false; 221 | this._allow_multiple = false; 222 | this._max_attachments = {}; 223 | this._allow_reload = true; 224 | this._allow_remove = true; 225 | this._display_ready = false; 226 | this._unprocessed_files = []; 227 | 228 | frappe.realtime.on( 229 | 'better_attach_console', 230 | function(ret) { Helpers.log(ret); } 231 | ); 232 | 233 | if (Helpers.isString(this.df.options)) 234 | this.df.options = Helpers.parseJson(this.df.options, {}); 235 | if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; 236 | } 237 | _update_options(force) { 238 | if (!force && this._ls_options) return; 239 | this._ls_options = !Helpers.isEmpty(this.df.options) ? Helpers.deepClone(this.df.options) : {}; 240 | let opts = {}; 241 | if (!Helpers.isEmpty(this._ls_options)) { 242 | opts = this._parse_options(this._ls_options); 243 | if (!opts.disabled) { 244 | if (Helpers.isArray(this._ls_options.users) && this._ls_options.users.length) { 245 | let users = Helpers.filter(this._ls_options.users, function(v) { 246 | return this.isPlainObject(v) && ( 247 | (this.isString(v.for) && v.for === frappe.session.user) 248 | || (this.isArray(v.for) && v.for.indexOf(frappe.session.user) >= 0) 249 | ); 250 | }); 251 | if (users.length) opts = Helpers.merge(opts, this._parse_options(users[0])); 252 | } else if (Helpers.isArray(this._ls_options.roles)) { 253 | let roles = Helpers.filter(this._ls_options.roles, function(v) { 254 | return this.isPlainObject(v) 255 | && (this.isString(v.for) || this.isArray(v.for)) 256 | && frappe.user.has_role(v.for); 257 | }); 258 | if (roles.length) opts = Helpers.merge(opts, this._parse_options(roles[0])); 259 | } 260 | } 261 | } 262 | this._options = !opts.disabled ? (opts.options || null) : null; 263 | this._reload_control(opts); 264 | } 265 | _parse_options(opts) { 266 | var tmp = {options: {restrictions: {}, extra: {}}}; 267 | tmp.disabled = Helpers.toBool(Helpers.ifNull(opts.disabled, false)); 268 | tmp.allow_reload = Helpers.toBool(Helpers.ifNull(opts.allow_reload, true)); 269 | tmp.allow_remove = Helpers.toBool(Helpers.ifNull(opts.allow_remove, true)); 270 | Helpers.each([ 271 | ['upload_notes', 's'], ['disable_auto_save', 'b'], 272 | ['allow_multiple', 'b'], ['disable_file_browser', 'b'], 273 | ['dialog_title', 's'], 274 | ], function(k) { 275 | tmp.options[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 276 | }, this); 277 | Helpers.each([ 278 | ['max_file_size', 'i'], ['allowed_file_types', 'a'], 279 | ['max_number_of_files', 'i'], ['crop_image_aspect_ratio', 'i'], 280 | ['as_public', 'b'], 281 | ], 282 | function(k) { 283 | tmp.options.restrictions[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 284 | }, this); 285 | Helpers.each([['allowed_filename', 'r']], function(k) { 286 | tmp.options.extra[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 287 | }, this); 288 | if (tmp.options.dialog_title == null) delete tmp.options.dialog_title; 289 | if (this._is_webform) tmp.options.disable_file_browser = true; 290 | this._parse_allowed_file_types(tmp.options); 291 | return tmp; 292 | } 293 | _parse_options_val(v, t) { 294 | if (Helpers.isEmpty(v)) v = null; 295 | if (t === 's') return v && (v = cstr(v)) && v.length ? v : null; 296 | if (t === 'b') return Helpers.toBool(Helpers.ifNull(v, false)); 297 | if (t === 'i') return v && (v = cint(v)) && !isNaN(v) && v > 0 ? v : null; 298 | if (t === 'a') return Helpers.toArray(v); 299 | if (t === 'r') 300 | return v && (Helpers.isRegExp(v) || ((v = cstr(v)) && v.length)) 301 | ? (v[0] === '/' ? new RegExp(v) : v) : null; 302 | return v; 303 | } 304 | _parse_allowed_file_types(opts) { 305 | opts.extra.allowed_file_types = []; 306 | if (!opts.restrictions.allowed_file_types.length) return; 307 | opts.restrictions.allowed_file_types = Helpers.filter( 308 | opts.restrictions.allowed_file_types, 309 | function(v) { 310 | if (this.isString(v)) { 311 | if (!v.length) return false; 312 | if (v[0] === '$') { 313 | opts.extra.allowed_file_types.push(new RegExp(v.substring(1))); 314 | return false; 315 | } 316 | if (v.substring(v.length - 2) === '/*') 317 | opts.extra.allowed_file_types.push(new RegExp(v.substring(0, v.length - 1) + '/(.*?)')); 318 | return true; 319 | } else if (this.isRegExp(v)) { 320 | opts.extra.allowed_file_types.push(v); 321 | } 322 | return false; 323 | } 324 | ); 325 | } 326 | _toggle_remove_button() { 327 | var show = this._allow_remove; 328 | this.$value && this.$value.find('[data-action="clear_attachment"]').toggle(show); 329 | if (!this._$list) return; 330 | this._$list_group.find('.ba-actions').each(function(i, el) { 331 | $(el).toggleClass('ba-hidden', !show); 332 | }); 333 | } 334 | _reload_control(opts) { 335 | if (this.upload_options) 336 | this.upload_options = this.image_upload_options = null; 337 | 338 | this._disable_auto_save = this._options && this._options.disable_auto_save; 339 | 340 | if (Helpers.ifNull(opts.allow_reload, true) !== this._allow_reload) 341 | this.toggle_reload(!this._allow_reload); 342 | if (Helpers.ifNull(opts.allow_remove, true) !== this._allow_remove) 343 | this.toggle_remove(!this._allow_remove); 344 | 345 | let allow_multiple = this._options && this._options.allow_multiple; 346 | if (allow_multiple === this._allow_multiple) return; 347 | this._allow_multiple = allow_multiple; 348 | this._set_max_attachments(); 349 | if (!this._display_ready) return; 350 | this._setup_display(true); 351 | if (!this._value.length) return; 352 | let value = this._value.pop(); 353 | if (!this._allow_multiple && this._value.length) { 354 | var failed = 0; 355 | this._remove_files(this._value, function(ret) { 356 | if (!cint(ret)) failed++; 357 | }); 358 | if (failed) Helpers.error('Unable to delete the uploaded attachments.'); 359 | } 360 | this._reset_value(); 361 | this.set_input(value); 362 | } 363 | _set_max_attachments() { 364 | if (!this.frm) return; 365 | let meta = frappe.get_meta(this.frm.doctype); 366 | if ( 367 | !this._allow_multiple || !this._options 368 | || Helpers.isEmpty(this._options.restrictions.max_number_of_files) 369 | ) { 370 | if (meta && this._max_attachments.meta != null) 371 | meta.max_attachments = this._max_attachments.meta; 372 | if (this.frm.meta && this._max_attachments.fmeta != null) 373 | this.frm.meta.max_attachments = this._max_attachments.fmeta; 374 | return; 375 | } 376 | let val = this._options.restrictions.max_number_of_files; 377 | if (meta && val > cint(meta.max_attachments)) { 378 | if (this._max_attachments.meta == null) 379 | this._max_attachments.meta = meta.max_attachments; 380 | meta.max_attachments = val; 381 | } 382 | if (this.frm.meta && val > cint(this.frm.meta.max_attachments)) { 383 | if (this._max_attachments.fmeta == null) 384 | this._max_attachments.fmeta = this.frm.meta.max_attachments; 385 | this.frm.meta.max_attachments = val; 386 | } 387 | } 388 | _set_image_upload_options() { 389 | if (this.image_upload_options) return; 390 | let opts = this.image_upload_options = Helpers.deepClone(this.upload_options), 391 | extra = []; 392 | if (Helpers.isEmpty(opts.restrictions.allowed_file_types)) 393 | opts.restrictions.allowed_file_types = ['image/*']; 394 | else 395 | opts.restrictions.allowed_file_types = Filetype.to_images_list( 396 | Helpers.toArray(opts.restrictions.allowed_file_types) 397 | ); 398 | if (!opts.extra) opts.extra = {}; 399 | else Helpers.each(opts.extra.allowed_file_types, function(v, i) { 400 | i = Helpers.isRegExp(v) ? '' + v.source : v; 401 | if (Helpers.isString(i) && Filetype.is_ext_image(i)) extra.push(v); 402 | }); 403 | opts.extra.allowed_file_types = extra; 404 | if (!opts.restrictions.crop_image_aspect_ratio) 405 | opts.restrictions.crop_image_aspect_ratio = 1; 406 | } 407 | _set_value(value) { 408 | if (this._value.includes(value)) return value; 409 | this._value.push(value); 410 | let idx = this._value.length - 1; 411 | if (this._allow_multiple) { 412 | this.value = Helpers.toJson(this._value); 413 | this._add_file(value, idx); 414 | value = this.value; 415 | } 416 | else if (!this._images_only) this._add_file(value, idx); 417 | return value; 418 | } 419 | _setup_display(reset) { 420 | if (this._allow_multiple) { 421 | if (reset) this._destroy_popover(); 422 | this._setup_list(); 423 | } else { 424 | if (reset) { 425 | this._destroy_list(); 426 | this._files.length && Helpers.clear(this._files); 427 | } 428 | this._setup_popover(); 429 | } 430 | this._display_ready = true; 431 | } 432 | _setup_popover() { 433 | if (this._popover_ready) return; 434 | this.$value.find('.attached-file-link') 435 | .popover({ 436 | trigger: 'hover', 437 | placement: 'top', 438 | content: Helpers.fnBind(function() { 439 | let file = !this._images_only ? this._files[this._files.length - 1] : null, 440 | url = file ? file.file_url : this.value; 441 | if ((file && file.class === 'image') || this._images_only) { 442 | return '
' 443 | + '' 444 | + '
'; 445 | } 446 | if (file) { 447 | if (file.class === 'video') { 448 | return ''; 452 | } 453 | if (file.class === 'audio') { 454 | return ''; 458 | } 459 | } 460 | return '
' 461 | + __("This file type has no preview.") 462 | + '
'; 463 | }, this), 464 | html: true 465 | }); 466 | this._popover_ready = true; 467 | } 468 | _destroy_popover() { 469 | if (this._popover_ready) 470 | try { 471 | this.$value.find('.attached-file-link').popover('dispose'); 472 | } catch(_) {} 473 | this._popover_ready = null; 474 | } 475 | _add_file(value, idx) { 476 | var val = { 477 | name: null, 478 | file_name: Filetype.get_filename(value), 479 | file_url: value, 480 | extension: null, 481 | type: null, 482 | size: 0, 483 | size_str: '', 484 | 'class': 'other', 485 | }; 486 | this._files[idx] = val; 487 | if ( 488 | this.file_uploader && this.file_uploader.uploader 489 | && this.file_uploader.uploader.files 490 | ) { 491 | Helpers.each(this.file_uploader.uploader.files, function(f) { 492 | if (!f.doc || f.doc.file_url !== val.file_url) return; 493 | val.name = f.doc.name; 494 | if (!f.file_obj) return false; 495 | if (!this.isEmpty(f.file_obj.file_name)) { 496 | val.file_name = f.file_obj.file_name; 497 | val.extension = Filetype.get_file_ext(val.file_name); 498 | if (this.isEmpty(f.file_obj.type)) 499 | val.type = Filetype.get_file_type(val.extension); 500 | val = Filetype.set_file_info(val); 501 | } 502 | if (!this.isEmpty(f.file_obj.type)) 503 | val.type = f.file_obj.type.toLowerCase().split(';')[0]; 504 | if (!this.isEmpty(f.file_obj.size)) { 505 | val.size = f.file_obj.size; 506 | val.size_str = this.formatSize(val.size); 507 | } 508 | return false; 509 | }); 510 | } 511 | if (Helpers.isEmpty(val.extension)) { 512 | val.extension = Filetype.get_file_ext(val.file_name); 513 | val = Filetype.set_file_info(val); 514 | } 515 | if (Helpers.isEmpty(val.type)) 516 | val.type = Filetype.get_file_type(val.extension); 517 | if (Helpers.isEmpty(val.name) && this.frm) { 518 | !this._multiple_values ? this._process_files(idx) 519 | : this._unprocessed_files.push(idx); 520 | } else { 521 | if (Helpers.isEmpty(val.name)) val.name = val.file_name; 522 | this._add_list_file(val, idx); 523 | } 524 | } 525 | _process_files(idx) { 526 | if (idx == null && !this._unprocessed_files.length) return; 527 | if (idx != null) { 528 | try { 529 | frappe.db.get_value( 530 | 'File', {file_url: this._files[idx].file_url}, 'name', 531 | Helpers.fnBind(function(ret) { 532 | if (Helpers.isPlainObject(ret) && ret.name) { 533 | this._files[idx].name = ret.name; 534 | if (this.frm && this.frm.attachments) 535 | this.frm.attachments.update_attachment(this._files[idx]); 536 | } 537 | this._add_list_file(this._files[idx], idx); 538 | }, this) 539 | ); 540 | } catch(_) { 541 | Helpers.error( 542 | 'Unable to get the File doctype entry name for the uploaded attachment ({0}).', 543 | [this._files[idx].name] 544 | ); 545 | } 546 | return; 547 | } 548 | var names = [], 549 | urls = []; 550 | Helpers.each(this._unprocessed_files, function(idx) { 551 | names.push(this._files[idx].name); 552 | urls.push(this._files[idx].file_url); 553 | }, this); 554 | frappe.db.get_list('File', { 555 | fields: ['name', 'file_url'], 556 | filters: {file_url: ['in', urls]}, 557 | limit: urls.length 558 | }).then(Helpers.fnBind(function(ret) { 559 | var data = {}; 560 | Helpers.each(Helpers.toArray(ret), function(v) { 561 | if (this.isPlainObject(v) && v.file_url) data[v.file_url] = v.name; 562 | }); 563 | Helpers.each(this._unprocessed_files, function(idx, i) { 564 | i = data[this._files[idx].file_url]; 565 | if (i) { 566 | this._files[idx].name = i; 567 | if (this.frm && this.frm.attachments) 568 | this.frm.attachments.update_attachment(this._files[idx]); 569 | } 570 | this._add_list_file(this._files[idx], idx); 571 | }, this); 572 | Helpers.clear(this._unprocessed_files); 573 | }, this)) 574 | .catch(function() { 575 | Helpers.error( 576 | 'Unable to get the File doctype entry name for the uploaded attachments ({0}).', 577 | [names.join(', ')] 578 | ); 579 | }); 580 | } 581 | _add_list_file(file, idx) { 582 | // Check if allowed multiple files or not 583 | if (!this._allow_multiple || !this._$list) return; 584 | let meta = '', 585 | rem = !this._allow_remove ? ' ba-hidden' : ''; 586 | if (file.size && file.size_str) 587 | meta = '
' + file.size_str + '
'; 588 | this._$list_group.append( 589 | '
  • ' 590 | + '
    ' 591 | + '
    ' 592 | + '
    ' 593 | + '
    ' 594 | + '' 595 | + file.file_name 596 | + '' 597 | + meta 598 | + '
    ' 599 | + '
    ' 600 | + '
    ' 601 | + '' 604 | + '
    ' 605 | + '
    ' 606 | + '
  • ' 607 | ); 608 | } 609 | _remove_files(data, callback, error) { 610 | if (!Helpers.isArray(data)) data = [data]; 611 | Helpers.request( 612 | 'remove_files', {files: data}, 613 | Helpers.fnBind(callback, this), 614 | Helpers.fnBind(error, this) 615 | ); 616 | } 617 | _remove_file_by_idx(idx) { 618 | let len = this._value.length; 619 | if (!this._allow_multiple || (len - 1) < idx) return; 620 | let url = this._value[idx]; 621 | this._value.splice(idx, 1); 622 | if (this._allow_multiple) this._files.splice(idx, 1); 623 | len--; 624 | this.value = len ? Helpers.toJson(this._value) : null; 625 | if (this._allow_multiple && this._$list) { 626 | let child = this._$list_group.find('li[data-file-idx="' + idx + '"]'); 627 | if (child.length) child.remove(); 628 | } 629 | this._remove_file_by_url(url); 630 | } 631 | _remove_file_by_url(url) { 632 | if (!this.frm || !this.frm.attachments) 633 | this._remove_files(url, function(ret) { 634 | if (!cint(ret)) Helpers.error('Unable to remove the uploaded attachment ({0}).', [url]); 635 | }); 636 | else this.frm.attachments.remove_attachment_by_filename( 637 | url, Helpers.fnBind(function() { 638 | this.parse_validate_and_set_in_model(this.value) 639 | .then(Helpers.fnBind(function() { 640 | this.refresh(); 641 | this._form_save(); 642 | }, this)); 643 | }, this) 644 | ); 645 | } 646 | _setup_list() { 647 | if (this._$list) return; 648 | $(this.$value.children()[0]).children().each(function(i, el) { 649 | $(el).addClass('ba-hidden'); 650 | }); 651 | this._$list = $( 652 | '
    ' 653 | + '
    ' 654 | + '' 656 | + '
    ' 657 | + '
    ' 658 | ).appendTo(this.input_area); 659 | this._$list_group = this._$list.find('ul.list-group'); 660 | var me = this; 661 | this._$list_group.click('.ba-remove', function() { 662 | let $el = $(this); 663 | if (!$el.hasClass('ba-remove')) return; 664 | let $parent = $el.parents('.ba-attachment'); 665 | if (!$parent.length) return; 666 | let idx = $parent.attr('data-file-idx'); 667 | if (!idx || !/[0-9]+/.test('' + idx)) return; 668 | idx = cint(idx); 669 | if (idx >= 0) me._remove_file_by_idx(idx); 670 | }); 671 | } 672 | _destroy_list() { 673 | if (this._$list) { 674 | this._$list.remove(); 675 | $(this.$value.children()[0]).children().each(function(i, el) { 676 | $(el).removeClass('ba-hidden'); 677 | }); 678 | } 679 | this._$list = this._$list_group = null; 680 | } 681 | _update_input(value, dataurl) { 682 | value = value || this._value[this._value.length - 1]; 683 | this.$input.toggle(false); 684 | let file_url_parts = value.match(/^([^:]+),(.+):(.+)$/), 685 | filename = null; 686 | if (file_url_parts) { 687 | filename = file_url_parts[1]; 688 | dataurl = file_url_parts[2] + ':' + file_url_parts[3]; 689 | } 690 | if (!filename) filename = dataurl ? value : value.split('/').pop(); 691 | let $link = this.$value.toggle(true).find('.attached-file-link'); 692 | if (!this._allow_multiple) $link.html(filename).attr('href', dataurl || value); 693 | else { 694 | $link.html(this._value.length > 1 695 | ? this._value.length + ' ' + __('files uploaded') 696 | : filename 697 | ).attr('href', '#'); 698 | if (this._$list && this._$list.hasClass('ba-hidden')) 699 | this._$list.removeClass('ba-hidden'); 700 | } 701 | } 702 | _reset_input() { 703 | this.dataurl = null; 704 | this.fileobj = null; 705 | this.set_input(null); 706 | this.parse_validate_and_set_in_model(null); 707 | this.refresh(); 708 | } 709 | _reset_value() { 710 | this.value = null; 711 | this.$input.toggle(true); 712 | this.$value.toggle(false); 713 | Helpers.clear(this._value); 714 | if (!this._allow_multiple) return; 715 | Helpers.clear(this._files); 716 | if (this._$list) 717 | this._$list_group.children().each(function(i, el) { 718 | $(el).remove(); 719 | }); 720 | } 721 | _form_save() { 722 | if (this._disable_auto_save) return; 723 | this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); 724 | } 725 | }; 726 | -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/controls/attach_image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.ui.form.ControlAttachImage = class ControlAttachImage extends frappe.ui.form.ControlAttach { 10 | _setup_control() { 11 | this._images_only = true; 12 | super._setup_control(); 13 | } 14 | }; -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/controls/v12/attach.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import Helpers from './../../utils'; 10 | import Filetype from './../../filetypes'; 11 | 12 | 13 | frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ 14 | make: function() { 15 | this._super(); 16 | this._setup_control(); 17 | }, 18 | make_input: function() { 19 | this._update_options(); 20 | this._super(); 21 | this._toggle_remove_button(); 22 | this._setup_display(); 23 | }, 24 | clear_attachment: function() { 25 | if (!this._allow_remove) return; 26 | if (!this.frm) { 27 | if (!this._value.length) this._reset_input(); 28 | else this._remove_files(this._value, function(ret) { 29 | if (cint(ret)) this._reset_input(); 30 | else Helpers.error('Unable to clear the uploaded attachments.'); 31 | }); 32 | return; 33 | } 34 | // To prevent changing value from within set_input function 35 | this._prevent_input = true; 36 | if (!this._value.length) { 37 | this._reset_value(); 38 | this.refresh(); 39 | this._form_save(); 40 | // To allow changing value from within set_input function 41 | this._prevent_input = false; 42 | return; 43 | } 44 | this._remove_files(this._value, function(ret) { 45 | if (!cint(ret)) return Helpers.error('Unable to clear the uploaded attachments.'); 46 | if (this.frm.attachments) 47 | Helpers.each(this._value, function(v) { 48 | var fid = this.frm.attachments.get_file_id_from_file_url(v); 49 | if (fid) this.frm.attachments.remove_fileid(fid); 50 | }, this); 51 | 52 | this.frm.sidebar && this.frm.sidebar.reload_docinfo(); 53 | try { 54 | this.parse_validate_and_set_in_model(null); 55 | this._reset_value(); 56 | this.refresh(); 57 | this._form_save(); 58 | // To allow changing value from within set_input function 59 | this._prevent_input = false; 60 | } catch(_) { 61 | // To allow changing value from within set_input function 62 | this._prevent_input = false; 63 | } 64 | }, function() { 65 | // To allow changing value from within set_input function before failure 66 | this._prevent_input = false; 67 | }); 68 | }, 69 | on_attach_click: function() { 70 | this.set_upload_options(); 71 | this.file_uploader = new frappe.ui.FileUploader(this.upload_options); 72 | }, 73 | set_upload_options: function() { 74 | if (this.upload_options) return; 75 | this._update_options(); 76 | var opts = this._options && this.df.options; 77 | if (opts) this.df.options = this._options; 78 | this._super(); 79 | if (opts) this.df.options = opts; 80 | if (this._images_only) this._set_image_upload_options(); 81 | }, 82 | set_value: function(value, force_set_value) { 83 | // Prevent changing value if called from event 84 | if (this._prevent_input) return Promise.resolve(); 85 | value = this._set_value(value); 86 | if (!this.frm) this._updating_input = true; 87 | return this._super(value, force_set_value || false); 88 | }, 89 | set_input: function(value, dataurl) { 90 | // Prevent changing value if called from event 91 | if (this._prevent_input) return; 92 | if (this._updating_input) { 93 | this._updating_input = false; 94 | if (this._value.length) this._update_input(); 95 | return; 96 | } 97 | if (value === null) { 98 | if (!this._value.length) this._reset_value(); 99 | else this._remove_files(this._value, function(ret) { 100 | if (cint(ret)) this._reset_value(); 101 | else Helpers.error('Unable to delete the uploaded attachments.'); 102 | }); 103 | return; 104 | } 105 | if (Helpers.isEmpty(value)) return; 106 | var val = Helpers.toArray(value, null); 107 | if (Helpers.isArray(val)) { 108 | if (!val.length) return; 109 | var update = 0; 110 | if (!this._allow_multiple) { 111 | value = val[0]; 112 | if (Helpers.isString(value) && this._value.indexOf(value) < 0) { 113 | this._set_value(value); 114 | update = 1; 115 | } 116 | } else { 117 | Helpers.each(val, function(v) { 118 | if (Helpers.isString(v) && this._value.indexOf(value) < 0) { 119 | this._set_value(v); 120 | update = 1; 121 | } 122 | }, this); 123 | } 124 | if (update) this._update_input(); 125 | this._multiple_values = false; 126 | this._process_files(); 127 | return; 128 | } 129 | if (!Helpers.isString(value)) return; 130 | this.value = this._set_value(value); 131 | this._update_input(value, dataurl); 132 | }, 133 | on_upload_complete: function(attachment) { 134 | if (this.frm) { 135 | this.parse_validate_and_set_in_model(attachment.file_url); 136 | this.frm.attachments && this.frm.attachments.update_attachment(attachment); 137 | if (!this._allow_multiple) this._form_save(); 138 | else { 139 | var up = this.file_uploader && this.file_uploader.uploader; 140 | if (up && up.files && up.files.every(function(file) { return !file.failed; })) 141 | this._form_save(); 142 | } 143 | } 144 | this.set_input(attachment.file_url); 145 | }, 146 | refresh: function() { 147 | this._super(); 148 | if (Helpers.isString(this.df.options)) 149 | this.df.options = Helpers.parseJson(this.df.options, {}); 150 | else if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; 151 | if (!Helpers.isEqual(this.df.options, this._ls_options)) 152 | this._update_options(true); 153 | this.set_input(Helpers.toArray(this.value)); 154 | }, 155 | // Custom Methods 156 | toggle_auto_save: function(enable) { 157 | if (enable != null) this._disable_auto_save = enable ? false : true; 158 | else this._disable_auto_save = !this._disable_auto_save; 159 | }, 160 | toggle_remove: function(allow) { 161 | if (allow != null) this._allow_remove = !!allow; 162 | else this._allow_remove = !this._allow_remove; 163 | this._toggle_remove_button(); 164 | }, 165 | set_options: function(opts) { 166 | if (Helpers.isString(opts) && opts.length) opts = Helpers.parseJson(opts, null); 167 | if (Helpers.isEmpty(opts) || !Helpers.isPlainObject(opts)) return; 168 | opts = Helpers.merge(this.df.options, opts); 169 | if (Helpers.isEqual(this.df.options, opts)) return; 170 | this.df.options = opts; 171 | this._update_options(true); 172 | }, 173 | // Private Methods 174 | _setup_control: function() { 175 | this._doctype = (this.frm && this.frm.doctype) 176 | || this.doctype 177 | || (this.doc && this.doc.doctype) 178 | || null; 179 | this._is_webform = (frappe.BAC && !!frappe.BAC.webform) 180 | || this._doctype === 'Web Form' 181 | || this.df.parenttype === 'Web Form' 182 | || this.df.is_web_form 183 | || (this.doc && this.doc.web_form_name); 184 | 185 | if (this._is_webform && frappe.BAC) { 186 | if (Helpers.isString(frappe.BAC.options)) 187 | frappe.BAC.options = Helpers.parseJson(frappe.BAC.options, {}); 188 | if (!Helpers.isPlainObject(frappe.BAC.options)) frappe.BAC.options = {}; 189 | if (frappe.BAC.options[this.df.fieldname]) 190 | this.df.options = frappe.BAC.options[this.df.fieldname]; 191 | } 192 | 193 | this._ls_options = null; 194 | this._options = null; 195 | this._value = []; 196 | this._files = []; 197 | this._disable_auto_save = false; 198 | this._allow_multiple = false; 199 | this._max_attachments = {}; 200 | this._allow_remove = true; 201 | this._display_ready = false; 202 | this._unprocessed_files = []; 203 | 204 | frappe.realtime.on( 205 | 'better_attach_console', 206 | function(ret) { Helpers.log(ret); } 207 | ); 208 | 209 | if (Helpers.isString(this.df.options)) 210 | this.df.options = Helpers.parseJson(this.df.options, {}); 211 | if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; 212 | }, 213 | _update_options: function(force) { 214 | if (!force && this._ls_options) return; 215 | this._ls_options = !Helpers.isEmpty(this.df.options) ? Helpers.deepClone(this.df.options) : {}; 216 | let opts = {}; 217 | if (!Helpers.isEmpty(this._ls_options)) { 218 | opts = this._parse_options(this._ls_options); 219 | if (!opts.disabled) { 220 | if (Helpers.isArray(this._ls_options.users) && this._ls_options.users.length) { 221 | let users = Helpers.filter(this._ls_options.users, function(v) { 222 | return this.isPlainObject(v) && ( 223 | (this.isString(v.for) && v.for === frappe.session.user) 224 | || (this.isArray(v.for) && v.for.indexOf(frappe.session.user) >= 0) 225 | ); 226 | }); 227 | if (users.length) opts = Helpers.merge(opts, this._parse_options(users[0])); 228 | } else if (Helpers.isArray(this._ls_options.roles)) { 229 | let roles = Helpers.filter(this._ls_options.roles, function(v) { 230 | return this.isPlainObject(v) 231 | && (this.isString(v.for) || this.isArray(v.for)) 232 | && frappe.user.has_role(v.for); 233 | }); 234 | if (roles.length) opts = Helpers.merge(opts, this._parse_options(roles[0])); 235 | } 236 | } 237 | } 238 | this._options = !opts.disabled ? (opts.options || null) : null; 239 | this._reload_control(opts); 240 | }, 241 | _parse_options: function(opts) { 242 | var tmp = {options: {restrictions: {}, extra: {}}}; 243 | tmp.disabled = Helpers.toBool(Helpers.ifNull(opts.disabled, false)); 244 | tmp.allow_remove = Helpers.toBool(Helpers.ifNull(opts.allow_remove, true)); 245 | Helpers.each([ 246 | ['upload_notes', 's'], ['disable_auto_save', 'b'], 247 | ['allow_multiple', 'b'], ['disable_file_browser', 'b'], 248 | ], function(k) { 249 | tmp.options[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 250 | }, this); 251 | Helpers.each([ 252 | ['max_file_size', 'i'], ['allowed_file_types', 'a'], 253 | ['max_number_of_files', 'i'], ['as_public', 'b'], 254 | ], 255 | function(k) { 256 | tmp.options.restrictions[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 257 | }, this); 258 | Helpers.each([['allowed_filename', 'r']], function(k) { 259 | tmp.options.extra[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 260 | }, this); 261 | if (this._is_webform) tmp.options.disable_file_browser = true; 262 | this._parse_allowed_file_types(tmp.options); 263 | return tmp; 264 | }, 265 | _parse_options_val: function(v, t) { 266 | if (Helpers.isEmpty(v)) v = null; 267 | if (t === 's') return v && (v = cstr(v)) && v.length ? v : null; 268 | if (t === 'b') return Helpers.toBool(Helpers.ifNull(v, false)); 269 | if (t === 'i') return v && (v = cint(v)) && !isNaN(v) && v > 0 ? v : null; 270 | if (t === 'a') return Helpers.toArray(v); 271 | if (t === 'r') 272 | return v && (Helpers.isRegExp(v) || ((v = cstr(v)) && v.length)) 273 | ? (v[0] === '/' ? new RegExp(v) : v) : null; 274 | return v; 275 | }, 276 | _parse_allowed_file_types: function(opts) { 277 | opts.extra.allowed_file_types = []; 278 | if (!opts.restrictions.allowed_file_types.length) return; 279 | opts.restrictions.allowed_file_types = Helpers.filter( 280 | opts.restrictions.allowed_file_types, 281 | function(v) { 282 | if (this.isString(v)) { 283 | if (!v.length) return false; 284 | if (v[0] === '$') { 285 | opts.extra.allowed_file_types.push(new RegExp(v.substring(1))); 286 | return false; 287 | } 288 | if (v.substring(v.length - 2) === '/*') 289 | opts.extra.allowed_file_types.push(new RegExp(v.substring(0, v.length - 1) + '/(.*?)')); 290 | return true; 291 | } else if (this.isRegExp(v)) { 292 | opts.extra.allowed_file_types.push(v); 293 | } 294 | return false; 295 | } 296 | ); 297 | }, 298 | _toggle_remove_button: function() { 299 | var show = this._allow_remove; 300 | this.$value && this.$value.find('[data-action="clear_attachment"]').toggle(show); 301 | if (!this._$list) return; 302 | this._$list_group.find('.ba-actions').each(function(i, el) { 303 | $(el).toggleClass('ba-hidden', !show); 304 | }); 305 | }, 306 | _reload_control: function(opts) { 307 | if (this.upload_options) this.upload_options = null; 308 | 309 | this._disable_auto_save = this._options && this._options.disable_auto_save; 310 | 311 | if (Helpers.ifNull(opts.allow_remove, true) !== this._allow_remove) 312 | this.toggle_remove(!this._allow_remove); 313 | 314 | var allow_multiple = this._options && this._options.allow_multiple; 315 | if (allow_multiple === this._allow_multiple) return; 316 | this._allow_multiple = allow_multiple; 317 | this._set_max_attachments(); 318 | if (!this._display_ready) return; 319 | this._setup_display(true); 320 | if (!this._value.length) return; 321 | var value = this._value.pop(); 322 | if (!this._allow_multiple && this._value.length) { 323 | var failed = 0; 324 | this._remove_files(this._value, function(ret) { 325 | if (!cint(ret)) failed++; 326 | }); 327 | if (failed) Helpers.error('Unable to delete the uploaded attachments.'); 328 | } 329 | this._reset_value(); 330 | this.set_input(value); 331 | }, 332 | _set_max_attachments: function() { 333 | if (!this.frm) return; 334 | var meta = frappe.get_meta(this.frm.doctype); 335 | if ( 336 | !this._allow_multiple || !Helpers.isPlainObject(this._options) 337 | || Helpers.isEmpty(this._options.restrictions.max_number_of_files) 338 | ) { 339 | if (meta && this._max_attachments.meta != null) 340 | meta.max_attachments = this._max_attachments.meta; 341 | if (this.frm.meta && this._max_attachments.fmeta != null) 342 | this.frm.meta.max_attachments = this._max_attachments.fmeta; 343 | return; 344 | } 345 | var val = this._options.restrictions.max_number_of_files; 346 | if (meta && val > cint(meta.max_attachments)) { 347 | if (this._max_attachments.meta == null) 348 | this._max_attachments.meta = meta.max_attachments; 349 | meta.max_attachments = val; 350 | } 351 | if (this.frm.meta && val > cint(this.frm.meta.max_attachments)) { 352 | if (this._max_attachments.fmeta == null) 353 | this._max_attachments.fmeta = this.frm.meta.max_attachments; 354 | this.frm.meta.max_attachments = val; 355 | } 356 | }, 357 | _set_image_upload_options: function() { 358 | let opts = Helpers.deepClone(this.upload_options), 359 | extra = []; 360 | if (Helpers.isEmpty(opts.restrictions.allowed_file_types)) 361 | opts.restrictions.allowed_file_types = ['image/*']; 362 | else 363 | opts.restrictions.allowed_file_types = Filetype.to_images_list( 364 | Helpers.toArray(opts.restrictions.allowed_file_types) 365 | ); 366 | if (!opts.extra) opts.extra = {}; 367 | else Helpers.each(opts.extra.allowed_file_types, function(v, i) { 368 | i = Helpers.isRegExp(v) ? '' + v.source : v; 369 | if (Helpers.isString(i) && Filetype.is_ext_image(i)) extra.push(v); 370 | }); 371 | opts.extra.allowed_file_types = extra; 372 | this.upload_options = opts; 373 | }, 374 | _set_value: function(value) { 375 | if (this._value.indexOf(value) >= 0) return value; 376 | this._value.push(value); 377 | var idx = this._value.length - 1; 378 | if (this._allow_multiple) { 379 | this.value = Helpers.toJson(this._value); 380 | this._add_file(value, idx); 381 | value = this.value; 382 | } 383 | else if (!this._images_only) this._add_file(value, idx); 384 | return value; 385 | }, 386 | _setup_display: function(reset) { 387 | if (this._allow_multiple) { 388 | if (reset) this._destroy_popover(); 389 | this._setup_list(); 390 | } else { 391 | if (reset) { 392 | this._destroy_list(); 393 | this._files.length && Helpers.clear(this._files); 394 | } 395 | this._setup_popover(); 396 | } 397 | this._display_ready = true; 398 | }, 399 | _setup_popover: function() { 400 | if (this._popover_ready) return; 401 | this.$value.find('.attached-file-link').first() 402 | .popover({ 403 | trigger: 'hover', 404 | placement: 'top', 405 | content: Helpers.fnBind(function() { 406 | var file = !this._images_only ? this._files[this._files.length - 1] : null, 407 | url = file ? file.file_url : this.value; 408 | if ((file && file.class === 'image') || this._images_only) { 409 | return '
    ' 410 | + '' 411 | + '
    '; 412 | } 413 | if (file) { 414 | if (file.class === 'video') { 415 | return ''; 419 | } 420 | if (file.class === 'audio') { 421 | return ''; 425 | } 426 | } 427 | return '
    ' 428 | + __("This file type has no preview.") 429 | + '
    '; 430 | }, this), 431 | html: true 432 | }); 433 | this._popover_ready = true; 434 | }, 435 | _destroy_popover: function() { 436 | if (this._popover_ready) 437 | try { 438 | this.$value.find('.attached-file-link').popover('dispose'); 439 | } catch(_) {} 440 | this._popover_ready = null; 441 | }, 442 | _add_file: function(value, idx) { 443 | var val = { 444 | name: null, 445 | file_name: Filetype.get_filename(value), 446 | file_url: value, 447 | extension: null, 448 | type: null, 449 | size: 0, 450 | size_str: '', 451 | 'class': 'other', 452 | }; 453 | this._files[idx] = val; 454 | if ( 455 | this.file_uploader && this.file_uploader.uploader 456 | && this.file_uploader.uploader.files 457 | ) { 458 | Helpers.each(this.file_uploader.uploader.files, function(f) { 459 | if (!f.doc || f.doc.file_url !== val.file_url) return; 460 | val.name = f.doc.name; 461 | if (f.file_obj) { 462 | if (!this.isEmpty(f.file_obj.file_name)) { 463 | val.file_name = f.file_obj.file_name; 464 | val.extension = Filetype.get_file_ext(val.file_name); 465 | if (this.isEmpty(f.file_obj.type)) 466 | val.type = Filetype.get_file_type(val.extension); 467 | val = Filetype.set_file_info(val); 468 | } 469 | if (!this.isEmpty(f.file_obj.type)) 470 | val.type = f.file_obj.type.toLowerCase().split(';')[0]; 471 | if (!this.isEmpty(f.file_obj.size)) { 472 | val.size = f.file_obj.size; 473 | val.size_str = this.formatSize(val.size); 474 | } 475 | } 476 | return false; 477 | }); 478 | } 479 | if (Helpers.isEmpty(val.extension)) { 480 | val.extension = Filetype.get_file_ext(val.file_name); 481 | val = Filetype.set_file_info(val); 482 | } 483 | if (Helpers.isEmpty(val.type)) 484 | val.type = Filetype.get_file_type(val.extension); 485 | if (Helpers.isEmpty(val.name) && this.frm) { 486 | if (!this._multiple_values) this._process_files(idx); 487 | else this._unprocessed_files.push(idx); 488 | } else { 489 | if (Helpers.isEmpty(val.name)) val.name = val.file_name; 490 | this._add_list_file(val, idx); 491 | } 492 | }, 493 | _process_files: function(idx) { 494 | if (idx == null && !this._unprocessed_files.length) return; 495 | if (idx != null) { 496 | try { 497 | frappe.db.get_value( 498 | 'File', {file_url: this._files[idx].file_url}, 'name', 499 | Helpers.fnBind(function(ret) { 500 | if (Helpers.isPlainObject(ret) && ret.name) { 501 | this._files[idx].name = ret.name; 502 | if (this.frm && this.frm.attachments) 503 | this.frm.attachments.update_attachment(this._files[idx]); 504 | } 505 | this._add_list_file(this._files[idx], idx); 506 | }, this) 507 | ); 508 | } catch(_) { 509 | Helpers.error( 510 | 'Unable to get the File doctype entry name for the uploaded attachment ({0}).', 511 | [this._files[idx].name] 512 | ); 513 | } 514 | return; 515 | } 516 | var names = [], 517 | urls = []; 518 | Helpers.each(this._unprocessed_files, function(idx) { 519 | names.push(this._files[idx].name); 520 | urls.push(this._files[idx].file_url); 521 | }, this); 522 | frappe.db.get_list('File', { 523 | fields: ['name', 'file_url'], 524 | filters: {file_url: ['in', urls]}, 525 | limit: urls.length 526 | }).then(Helpers.fnBind(function(ret) { 527 | var data = {}; 528 | Helpers.each(Helpers.toArray(ret), function(v) { 529 | if (this.isPlainObject(v) && v.file_url) data[v.file_url] = v.name; 530 | }); 531 | Helpers.each(this._unprocessed_files, function(idx, i) { 532 | i = data[this._files[idx].file_url]; 533 | if (i) { 534 | this._files[idx].name = i; 535 | if (this.frm && this.frm.attachments) 536 | this.frm.attachments.update_attachment(this._files[idx]); 537 | } 538 | this._add_list_file(this._files[idx], idx); 539 | }, this); 540 | Helpers.clear(this._unprocessed_files); 541 | }, this)) 542 | .catch(function() { 543 | Helpers.error( 544 | 'Unable to get the File doctype entry name for the uploaded attachments ({0}).', 545 | [names.join(', ')] 546 | ); 547 | }); 548 | }, 549 | _add_list_file: function(file, idx) { 550 | // Check if allowed multiple files or not 551 | if (!this._allow_multiple || !this._$list) return; 552 | var meta = '', 553 | rem = !this._allow_remove ? ' ba-hidden' : ''; 554 | if (file.size && file.size_str) 555 | meta = '
    ' + file.size_str + '
    '; 556 | this._$list_group.append( 557 | '
  • ' 558 | + '
    ' 559 | + '
    ' 560 | + '
    ' 561 | + '
    ' 562 | + '' 563 | + file.file_name 564 | + '' 565 | + meta 566 | + '
    ' 567 | + '
    ' 568 | + '
    ' 569 | + '' 572 | + '
    ' 573 | + '
    ' 574 | + '
  • ' 575 | ); 576 | }, 577 | _remove_files: function(data, callback, error) { 578 | if (!Helpers.isArray(data)) data = [data]; 579 | Helpers.request( 580 | 'remove_files', {files: data}, 581 | Helpers.fnBind(callback, this), 582 | Helpers.fnBind(error, this) 583 | ); 584 | }, 585 | _remove_file_by_idx: function(idx) { 586 | var len = this._value.length; 587 | if (!this._allow_multiple || (len - 1) < idx) return; 588 | var url = this._value[idx]; 589 | this._value.splice(idx, 1); 590 | if (this._allow_multiple) this._files.splice(idx, 1); 591 | len--; 592 | this.value = len ? Helpers.toJson(this._value) : null; 593 | if (this._allow_multiple && this._$list) { 594 | var child = this._$list_group.find('li[data-file-idx="' + idx + '"]'); 595 | if (child.length) child.remove(); 596 | } 597 | this._remove_file_by_url(url); 598 | }, 599 | _remove_file_by_url: function(url) { 600 | if (!this.frm || !this.frm.attachments) 601 | this._remove_files(url, function(ret) { 602 | if (!cint(ret)) Helpers.error('Unable to remove the uploaded attachment ({0}).', [url]); 603 | }); 604 | else this.frm.attachments.remove_attachment_by_filename( 605 | url, Helpers.fnBind(function() { 606 | this.parse_validate_and_set_in_model(this.value); 607 | this.refresh(); 608 | this._form_save(); 609 | }, this) 610 | ); 611 | }, 612 | _setup_list: function() { 613 | if (this._$list) return; 614 | $(this.$value.children()[0]).children().each(function(i, el) { 615 | $(el).addClass('ba-hidden'); 616 | }); 617 | this._$list = $( 618 | '
    ' 619 | + '
    ' 620 | + '' 622 | + '
    ' 623 | + '
    ' 624 | ).appendTo(this.input_area); 625 | this._$list_group = this._$list.find('ul.list-group'); 626 | var me = this; 627 | this._$list_group.click('.ba-remove', function() { 628 | var $el = $(this); 629 | if (!$el.hasClass('ba-remove')) return; 630 | var $parent = $el.parents('.ba-attachment'); 631 | if (!$parent.length) return; 632 | var idx = $parent.attr('data-file-idx'); 633 | if (!idx || !/[0-9]+/.test('' + idx)) return; 634 | idx = cint(idx); 635 | if (idx >= 0) me._remove_file_by_idx(idx); 636 | }); 637 | }, 638 | _destroy_list: function() { 639 | if (this._$list) { 640 | this._$list.remove(); 641 | $(this.$value.children()[0]).children().each(function(i, el) { 642 | $(el).removeClass('ba-hidden'); 643 | }); 644 | } 645 | this._$list = this._$list_group = null; 646 | }, 647 | _update_input: function(value, dataurl) { 648 | value = value || this._value[this._value.length - 1]; 649 | this.$input.toggle(false); 650 | var file_url_parts = value.match(/^([^:]+),(.+):(.+)$/), 651 | filename = null; 652 | if (file_url_parts) { 653 | filename = file_url_parts[1]; 654 | dataurl = file_url_parts[2] + ':' + file_url_parts[3]; 655 | } 656 | if (!filename) filename = dataurl ? value : value.split('/').pop(); 657 | var $link = this.$value.toggle(true).find('.attached-file-link'); 658 | if (!this._allow_multiple) $link.html(filename).attr('href', dataurl || value); 659 | else { 660 | $link.html(this._value.length > 1 661 | ? this._value.length + ' ' + __('files uploaded') 662 | : filename 663 | ).attr('href', '#'); 664 | if (this._$list && this._$list.hasClass('ba-hidden')) 665 | this._$list.removeClass('ba-hidden'); 666 | } 667 | }, 668 | _reset_input: function() { 669 | this.dataurl = null; 670 | this.fileobj = null; 671 | this.set_input(null); 672 | this.parse_validate_and_set_in_model(null); 673 | this.refresh(); 674 | }, 675 | _reset_value: function() { 676 | this.value = null; 677 | this.$input.toggle(true); 678 | this.$value.toggle(false); 679 | Helpers.clear(this._value); 680 | if (!this._allow_multiple) return; 681 | Helpers.clear(this._files); 682 | if (this._$list) 683 | this._$list_group.children().each(function(i, el) { 684 | $(el).remove(); 685 | }); 686 | }, 687 | _form_save: function() { 688 | if (this._disable_auto_save) return; 689 | this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); 690 | } 691 | }); -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/controls/v12/attach_image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({ 10 | _setup_control: function() { 11 | this._images_only = true; 12 | this._super(); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/controls/v13/attach.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import Helpers from './../../utils'; 10 | import Filetype from './../../filetypes'; 11 | 12 | 13 | frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ 14 | make: function() { 15 | this._super(); 16 | this._setup_control(); 17 | }, 18 | make_input: function() { 19 | this._update_options(); 20 | this._super(); 21 | this._toggle_remove_button(); 22 | this._setup_display(); 23 | }, 24 | clear_attachment: function() { 25 | if (!this._allow_remove) return; 26 | if (!this.frm) { 27 | if (!this._value.length) this._reset_input(); 28 | else this._remove_files(this._value, function(ret) { 29 | if (cint(ret)) this._reset_input(); 30 | else Helpers.error('Unable to clear the uploaded attachments.'); 31 | }); 32 | return; 33 | } 34 | // To prevent changing value from within set_input function 35 | this._prevent_input = true; 36 | if (!this._value.length) { 37 | this._reset_value(); 38 | this.refresh(); 39 | this._form_save(); 40 | // To allow changing value from within set_input function 41 | this._prevent_input = false; 42 | return; 43 | } 44 | this._remove_files(this._value, function(ret) { 45 | if (!cint(ret)) return Helpers.error('Unable to clear the uploaded attachments.'); 46 | if (this.frm.attachments) 47 | Helpers.each(this._value, function(v) { 48 | var fid = this.frm.attachments.get_file_id_from_file_url(v); 49 | if (fid) this.frm.attachments.remove_fileid(fid); 50 | }, this); 51 | 52 | this.frm.sidebar && this.frm.sidebar.reload_docinfo(); 53 | try { 54 | this.parse_validate_and_set_in_model(null); 55 | this._reset_value(); 56 | this.refresh(); 57 | this._form_save(); 58 | // To allow changing value from within set_input function 59 | this._prevent_input = false; 60 | } catch(_) { 61 | // To allow changing value from within set_input function 62 | this._prevent_input = false; 63 | } 64 | }, function() { 65 | // To allow changing value from within set_input function before failure 66 | this._prevent_input = false; 67 | }); 68 | }, 69 | reload_attachment: function() { 70 | this._allow_reload && this._super(); 71 | }, 72 | on_attach_click: function() { 73 | this.set_upload_options(); 74 | this.file_uploader = new frappe.ui.FileUploader(this.upload_options); 75 | }, 76 | set_upload_options: function() { 77 | if (this.upload_options) return; 78 | this._update_options(); 79 | var opts = this._options && this.df.options; 80 | if (opts) this.df.options = this._options; 81 | this._super(); 82 | if (opts) this.df.options = opts; 83 | if (this._images_only) this._set_image_upload_options(); 84 | }, 85 | set_value: function(value, force_set_value) { 86 | // Prevent changing value if called from event 87 | if (this._prevent_input) return Promise.resolve(); 88 | value = this._set_value(value); 89 | if (!this.frm) this._updating_input = true; 90 | return this._super(value, force_set_value || false); 91 | }, 92 | set_input: function(value, dataurl) { 93 | // Prevent changing value if called from event 94 | if (this._prevent_input) return; 95 | if (this._updating_input) { 96 | this._updating_input = false; 97 | if (this._value.length) this._update_input(); 98 | return; 99 | } 100 | if (value === null) { 101 | if (!this._value.length) this._reset_value(); 102 | else this._remove_files(this._value, function(ret) { 103 | if (cint(ret)) this._reset_value(); 104 | else Helpers.error('Unable to delete the uploaded attachments.'); 105 | }); 106 | return; 107 | } 108 | if (Helpers.isEmpty(value)) return; 109 | var val = Helpers.toArray(value, null); 110 | if (Helpers.isArray(val)) { 111 | if (!val.length) return; 112 | var update = 0; 113 | if (!this._allow_multiple) { 114 | value = val[0]; 115 | if (Helpers.isString(value) && this._value.indexOf(value) < 0) { 116 | this._set_value(value); 117 | update = 1; 118 | } 119 | } else { 120 | Helpers.each(val, function(v) { 121 | if (Helpers.isString(v) && this._value.indexOf(value) < 0) { 122 | this._set_value(v); 123 | update = 1; 124 | } 125 | }, this); 126 | } 127 | if (update) this._update_input(); 128 | this._multiple_values = false; 129 | this._process_files(); 130 | return; 131 | } 132 | if (!Helpers.isString(value)) return; 133 | this.value = this._set_value(value); 134 | this._update_input(value, dataurl); 135 | }, 136 | on_upload_complete: function(attachment) { 137 | if (this.frm) { 138 | this.parse_validate_and_set_in_model(attachment.file_url); 139 | this.frm.attachments && this.frm.attachments.update_attachment(attachment); 140 | if (!this._allow_multiple) this._form_save(); 141 | else { 142 | var up = this.file_uploader && this.file_uploader.uploader; 143 | if (up && up.files && up.files.every(function(file) { return !file.failed; })) 144 | this._form_save(); 145 | } 146 | } 147 | this.set_value(attachment.file_url); 148 | }, 149 | toggle_reload_button: function() { 150 | if (!this.$value) return; 151 | var show = this._allow_reload && this.file_uploader 152 | && this.file_uploader.uploader.files 153 | && this.file_uploader.uploader.files.length > 0; 154 | this.$value.find('[data-action="reload_attachment"]').toggle(show); 155 | }, 156 | refresh: function() { 157 | this._super(); 158 | if (Helpers.isString(this.df.options)) 159 | this.df.options = Helpers.parseJson(this.df.options, {}); 160 | else if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; 161 | if (!Helpers.isEqual(this.df.options, this._ls_options)) 162 | this._update_options(true); 163 | this.set_input(Helpers.toArray(this.value)); 164 | }, 165 | // Custom Methods 166 | toggle_auto_save: function(enable) { 167 | if (enable != null) this._disable_auto_save = enable ? false : true; 168 | else this._disable_auto_save = !this._disable_auto_save; 169 | }, 170 | toggle_reload: function(allow) { 171 | if (allow != null) this._allow_reload = !!allow; 172 | else this._allow_reload = !this._allow_reload; 173 | this.toggle_reload_button(); 174 | }, 175 | toggle_remove: function(allow) { 176 | if (allow != null) this._allow_remove = !!allow; 177 | else this._allow_remove = !this._allow_remove; 178 | this._toggle_remove_button(); 179 | }, 180 | set_options: function(opts) { 181 | if (Helpers.isString(opts) && opts.length) opts = Helpers.parseJson(opts, null); 182 | if (Helpers.isEmpty(opts) || !Helpers.isPlainObject(opts)) return; 183 | opts = Helpers.merge(this.df.options, opts); 184 | if (Helpers.isEqual(this.df.options, opts)) return; 185 | this.df.options = opts; 186 | this._update_options(true); 187 | }, 188 | // Private Methods 189 | _setup_control: function() { 190 | this._doctype = (this.frm && this.frm.doctype) 191 | || this.doctype 192 | || (this.doc && this.doc.doctype) 193 | || null; 194 | this._is_webform = (frappe.BAC && !!frappe.BAC.webform) 195 | || this._doctype === 'Web Form' 196 | || this.df.parenttype === 'Web Form' 197 | || this.df.is_web_form 198 | || (this.doc && this.doc.web_form_name); 199 | 200 | if (this._is_webform && frappe.BAC) { 201 | if (Helpers.isString(frappe.BAC.options)) 202 | frappe.BAC.options = Helpers.parseJson(frappe.BAC.options, {}); 203 | if (!Helpers.isPlainObject(frappe.BAC.options)) frappe.BAC.options = {}; 204 | if (frappe.BAC.options[this.df.fieldname]) 205 | this.df.options = frappe.BAC.options[this.df.fieldname]; 206 | } 207 | 208 | this._ls_options = null; 209 | this._options = null; 210 | this._value = []; 211 | this._files = []; 212 | this._disable_auto_save = false; 213 | this._allow_multiple = false; 214 | this._max_attachments = {}; 215 | this._allow_reload = true; 216 | this._allow_remove = true; 217 | this._display_ready = false; 218 | this._unprocessed_files = []; 219 | 220 | frappe.realtime.on( 221 | 'better_attach_console', 222 | function(ret) { Helpers.log(ret); } 223 | ); 224 | 225 | if (Helpers.isString(this.df.options)) 226 | this.df.options = Helpers.parseJson(this.df.options, {}); 227 | if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; 228 | }, 229 | _update_options: function(force) { 230 | if (!force && this._ls_options) return; 231 | this._ls_options = !Helpers.isEmpty(this.df.options) ? Helpers.deepClone(this.df.options) : {}; 232 | let opts = {}; 233 | if (!Helpers.isEmpty(this._ls_options)) { 234 | opts = this._parse_options(this._ls_options); 235 | if (!opts.disabled) { 236 | if (Helpers.isArray(this._ls_options.users) && this._ls_options.users.length) { 237 | let users = Helpers.filter(this._ls_options.users, function(v) { 238 | return this.isPlainObject(v) && ( 239 | (this.isString(v.for) && v.for === frappe.session.user) 240 | || (this.isArray(v.for) && v.for.indexOf(frappe.session.user) >= 0) 241 | ); 242 | }); 243 | if (users.length) opts = Helpers.merge(opts, this._parse_options(users[0])); 244 | } else if (Helpers.isArray(this._ls_options.roles)) { 245 | let roles = Helpers.filter(this._ls_options.roles, function(v) { 246 | return this.isPlainObject(v) 247 | && (this.isString(v.for) || this.isArray(v.for)) 248 | && frappe.user.has_role(v.for); 249 | }); 250 | if (roles.length) opts = Helpers.merge(opts, this._parse_options(roles[0])); 251 | } 252 | } 253 | } 254 | this._options = !opts.disabled ? (opts.options || null) : null; 255 | this._reload_control(opts); 256 | }, 257 | _parse_options: function(opts) { 258 | var tmp = {options: {restrictions: {}, extra: {}}}; 259 | tmp.disabled = Helpers.toBool(Helpers.ifNull(opts.disabled, false)); 260 | tmp.allow_reload = Helpers.toBool(Helpers.ifNull(opts.allow_reload, true)); 261 | tmp.allow_remove = Helpers.toBool(Helpers.ifNull(opts.allow_remove, true)); 262 | Helpers.each([ 263 | ['upload_notes', 's'], ['disable_auto_save', 'b'], 264 | ['allow_multiple', 'b'], ['disable_file_browser', 'b'], 265 | ], function(k) { 266 | tmp.options[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 267 | }, this); 268 | Helpers.each([ 269 | ['max_file_size', 'i'], ['allowed_file_types', 'a'], 270 | ['max_number_of_files', 'i'], ['as_public', 'b'], 271 | ], 272 | function(k) { 273 | tmp.options.restrictions[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 274 | }, this); 275 | Helpers.each([['allowed_filename', 'r']], function(k) { 276 | tmp.options.extra[k[0]] = this._parse_options_val(opts[k[0]], k[1]); 277 | }, this); 278 | if (this._is_webform) tmp.options.disable_file_browser = true; 279 | this._parse_allowed_file_types(tmp.options); 280 | return tmp; 281 | }, 282 | _parse_options_val: function(v, t) { 283 | if (Helpers.isEmpty(v)) v = null; 284 | if (t === 's') return v && (v = cstr(v)) && v.length ? v : null; 285 | if (t === 'b') return Helpers.toBool(Helpers.ifNull(v, false)); 286 | if (t === 'i') return v && (v = cint(v)) && !isNaN(v) && v > 0 ? v : null; 287 | if (t === 'a') return Helpers.toArray(v); 288 | if (t === 'r') 289 | return v && (Helpers.isRegExp(v) || ((v = cstr(v)) && v.length)) 290 | ? (v[0] === '/' ? new RegExp(v) : v) : null; 291 | return v; 292 | }, 293 | _parse_allowed_file_types: function(opts) { 294 | opts.extra.allowed_file_types = []; 295 | if (!opts.restrictions.allowed_file_types.length) return; 296 | opts.restrictions.allowed_file_types = Helpers.filter( 297 | opts.restrictions.allowed_file_types, 298 | function(v) { 299 | if (this.isString(v)) { 300 | if (!v.length) return false; 301 | if (v[0] === '$') { 302 | opts.extra.allowed_file_types.push(new RegExp(v.substring(1))); 303 | return false; 304 | } 305 | if (v.substring(v.length - 2) === '/*') 306 | opts.extra.allowed_file_types.push(new RegExp(v.substring(0, v.length - 1) + '/(.*?)')); 307 | return true; 308 | } else if (this.isRegExp(v)) { 309 | opts.extra.allowed_file_types.push(v); 310 | } 311 | return false; 312 | } 313 | ); 314 | }, 315 | _toggle_remove_button: function() { 316 | var show = this._allow_remove; 317 | this.$value && this.$value.find('[data-action="clear_attachment"]').toggle(show); 318 | if (!this._$list) return; 319 | this._$list_group.find('.ba-actions').each(function(i, el) { 320 | $(el).toggleClass('ba-hidden', !show); 321 | }); 322 | }, 323 | _reload_control: function(opts) { 324 | if (this.upload_options) this.upload_options = null; 325 | 326 | this._disable_auto_save = this._options && this._options.disable_auto_save; 327 | 328 | if (Helpers.ifNull(opts.allow_reload, true) !== this._allow_reload) 329 | this.toggle_reload(!this._allow_reload); 330 | if (Helpers.ifNull(opts.allow_remove, true) !== this._allow_remove) 331 | this.toggle_remove(!this._allow_remove); 332 | 333 | var allow_multiple = this._options && this._options.allow_multiple; 334 | if (allow_multiple === this._allow_multiple) return; 335 | this._allow_multiple = allow_multiple; 336 | this._set_max_attachments(); 337 | if (!this._display_ready) return; 338 | this._setup_display(true); 339 | if (!this._value.length) return; 340 | var value = this._value.pop(); 341 | if (!this._allow_multiple && this._value.length) { 342 | var failed = 0; 343 | this._remove_files(this._value, function(ret) { 344 | if (!cint(ret)) failed++; 345 | }); 346 | if (failed) Helpers.error('Unable to delete the uploaded attachments.'); 347 | } 348 | this._reset_value(); 349 | this.set_input(value); 350 | }, 351 | _set_max_attachments: function() { 352 | if (!this.frm) return; 353 | var meta = frappe.get_meta(this.frm.doctype); 354 | if ( 355 | !this._allow_multiple || !Helpers.isPlainObject(this._options) 356 | || Helpers.isEmpty(this._options.restrictions.max_number_of_files) 357 | ) { 358 | if (meta && this._max_attachments.meta != null) 359 | meta.max_attachments = this._max_attachments.meta; 360 | if (this.frm.meta && this._max_attachments.fmeta != null) 361 | this.frm.meta.max_attachments = this._max_attachments.fmeta; 362 | return; 363 | } 364 | var val = this._options.restrictions.max_number_of_files; 365 | if (meta && val > cint(meta.max_attachments)) { 366 | if (this._max_attachments.meta == null) 367 | this._max_attachments.meta = meta.max_attachments; 368 | meta.max_attachments = val; 369 | } 370 | if (this.frm.meta && val > cint(this.frm.meta.max_attachments)) { 371 | if (this._max_attachments.fmeta == null) 372 | this._max_attachments.fmeta = this.frm.meta.max_attachments; 373 | this.frm.meta.max_attachments = val; 374 | } 375 | }, 376 | _set_image_upload_options: function() { 377 | let opts = Helpers.deepClone(this.upload_options), 378 | extra = []; 379 | if (Helpers.isEmpty(opts.restrictions.allowed_file_types)) 380 | opts.restrictions.allowed_file_types = ['image/*']; 381 | else 382 | opts.restrictions.allowed_file_types = Filetype.to_images_list( 383 | Helpers.toArray(opts.restrictions.allowed_file_types) 384 | ); 385 | if (!opts.extra) opts.extra = {}; 386 | else Helpers.each(opts.extra.allowed_file_types, function(v, i) { 387 | i = Helpers.isRegExp(v) ? '' + v.source : v; 388 | if (Helpers.isString(i) && Filetype.is_ext_image(i)) extra.push(v); 389 | }); 390 | opts.extra.allowed_file_types = extra; 391 | this.upload_options = opts; 392 | }, 393 | _set_value: function(value) { 394 | if (this._value.indexOf(value) >= 0) return value; 395 | this._value.push(value); 396 | var idx = this._value.length - 1; 397 | if (this._allow_multiple) { 398 | this.value = Helpers.toJson(this._value); 399 | this._add_file(value, idx); 400 | value = this.value; 401 | } 402 | else if (!this._images_only) this._add_file(value, idx); 403 | return value; 404 | }, 405 | _setup_display: function(reset) { 406 | if (this._allow_multiple) { 407 | if (reset) this._destroy_popover(); 408 | this._setup_list(); 409 | } else { 410 | if (reset) { 411 | this._destroy_list(); 412 | this._files.length && Helpers.clear(this._files); 413 | } 414 | this._setup_popover(); 415 | } 416 | this._display_ready = true; 417 | }, 418 | _setup_popover: function() { 419 | if (this._popover_ready) return; 420 | this.$value.find('.attached-file-link').first() 421 | .popover({ 422 | trigger: 'hover', 423 | placement: 'top', 424 | content: Helpers.fnBind(function() { 425 | var file = !this._images_only ? this._files[this._files.length - 1] : null, 426 | url = file ? file.file_url : this.value; 427 | if ((file && file.class === 'image') || this._images_only) { 428 | return '
    ' 429 | + '' 430 | + '
    '; 431 | } 432 | if (file) { 433 | if (file.class === 'video') { 434 | return ''; 438 | } 439 | if (file.class === 'audio') { 440 | return ''; 444 | } 445 | } 446 | return '
    ' 447 | + __("This file type has no preview.") 448 | + '
    '; 449 | }, this), 450 | html: true 451 | }); 452 | this._popover_ready = true; 453 | }, 454 | _destroy_popover: function() { 455 | if (this._popover_ready) 456 | try { 457 | this.$value.find('.attached-file-link').popover('dispose'); 458 | } catch(_) {} 459 | this._popover_ready = null; 460 | }, 461 | _add_file: function(value, idx) { 462 | var val = { 463 | name: null, 464 | file_name: Filetype.get_filename(value), 465 | file_url: value, 466 | extension: null, 467 | type: null, 468 | size: 0, 469 | size_str: '', 470 | 'class': 'other', 471 | }; 472 | this._files[idx] = val; 473 | if ( 474 | this.file_uploader && this.file_uploader.uploader 475 | && this.file_uploader.uploader.files 476 | ) { 477 | Helpers.each(this.file_uploader.uploader.files, function(f) { 478 | if (!f.doc || f.doc.file_url !== val.file_url) return; 479 | val.name = f.doc.name; 480 | if (f.file_obj) { 481 | if (!this.isEmpty(f.file_obj.file_name)) { 482 | val.file_name = f.file_obj.file_name; 483 | val.extension = Filetype.get_file_ext(val.file_name); 484 | if (this.isEmpty(f.file_obj.type)) 485 | val.type = Filetype.get_file_type(val.extension); 486 | val = Filetype.set_file_info(val); 487 | } 488 | if (!this.isEmpty(f.file_obj.type)) 489 | val.type = f.file_obj.type.toLowerCase().split(';')[0]; 490 | if (!this.isEmpty(f.file_obj.size)) { 491 | val.size = f.file_obj.size; 492 | val.size_str = this.formatSize(val.size); 493 | } 494 | } 495 | return false; 496 | }); 497 | } 498 | if (Helpers.isEmpty(val.extension)) { 499 | val.extension = Filetype.get_file_ext(val.file_name); 500 | val = Filetype.set_file_info(val); 501 | } 502 | if (Helpers.isEmpty(val.type)) 503 | val.type = Filetype.get_file_type(val.extension); 504 | if (Helpers.isEmpty(val.name) && this.frm) { 505 | if (!this._multiple_values) this._process_files(idx); 506 | else this._unprocessed_files.push(idx); 507 | } else { 508 | if (Helpers.isEmpty(val.name)) val.name = val.file_name; 509 | this._add_list_file(val, idx); 510 | } 511 | }, 512 | _process_files: function(idx) { 513 | if (idx == null && !this._unprocessed_files.length) return; 514 | if (idx != null) { 515 | try { 516 | frappe.db.get_value( 517 | 'File', {file_url: this._files[idx].file_url}, 'name', 518 | Helpers.fnBind(function(ret) { 519 | if (Helpers.isPlainObject(ret) && ret.name) { 520 | this._files[idx].name = ret.name; 521 | if (this.frm && this.frm.attachments) 522 | this.frm.attachments.update_attachment(this._files[idx]); 523 | } 524 | this._add_list_file(this._files[idx], idx); 525 | }, this) 526 | ); 527 | } catch(_) { 528 | Helpers.error( 529 | 'Unable to get the File doctype entry name for the uploaded attachment ({0}).', 530 | [this._files[idx].name] 531 | ); 532 | } 533 | return; 534 | } 535 | var names = [], 536 | urls = []; 537 | Helpers.each(this._unprocessed_files, function(idx) { 538 | names.push(this._files[idx].name); 539 | urls.push(this._files[idx].file_url); 540 | }, this); 541 | frappe.db.get_list('File', { 542 | fields: ['name', 'file_url'], 543 | filters: {file_url: ['in', urls]}, 544 | limit: urls.length 545 | }).then(Helpers.fnBind(function(ret) { 546 | var data = {}; 547 | Helpers.each(Helpers.toArray(ret), function(v) { 548 | if (this.isPlainObject(v) && v.file_url) data[v.file_url] = v.name; 549 | }); 550 | Helpers.each(this._unprocessed_files, function(idx, i) { 551 | i = data[this._files[idx].file_url]; 552 | if (i) { 553 | this._files[idx].name = i; 554 | if (this.frm && this.frm.attachments) 555 | this.frm.attachments.update_attachment(this._files[idx]); 556 | } 557 | this._add_list_file(this._files[idx], idx); 558 | }, this); 559 | Helpers.clear(this._unprocessed_files); 560 | }, this)) 561 | .catch(function() { 562 | Helpers.error( 563 | 'Unable to get the File doctype entry name for the uploaded attachments ({0}).', 564 | [names.join(', ')] 565 | ); 566 | }); 567 | }, 568 | _add_list_file: function(file, idx) { 569 | // Check if allowed multiple files or not 570 | if (!this._allow_multiple || !this._$list) return; 571 | var meta = '', 572 | rem = !this._allow_remove ? ' ba-hidden' : ''; 573 | if (file.size && file.size_str) 574 | meta = '
    ' + file.size_str + '
    '; 575 | this._$list_group.append( 576 | '
  • ' 577 | + '
    ' 578 | + '
    ' 579 | + '
    ' 580 | + '
    ' 581 | + '' 582 | + file.file_name 583 | + '' 584 | + meta 585 | + '
    ' 586 | + '
    ' 587 | + '
    ' 588 | + '' 591 | + '
    ' 592 | + '
    ' 593 | + '
  • ' 594 | ); 595 | }, 596 | _remove_files: function(data, callback, error) { 597 | if (!Helpers.isArray(data)) data = [data]; 598 | Helpers.request( 599 | 'remove_files', {files: data}, 600 | Helpers.fnBind(callback, this), 601 | Helpers.fnBind(error, this) 602 | ); 603 | }, 604 | _remove_file_by_idx: function(idx) { 605 | var len = this._value.length; 606 | if (!this._allow_multiple || (len - 1) < idx) return; 607 | var url = this._value[idx]; 608 | this._value.splice(idx, 1); 609 | if (this._allow_multiple) this._files.splice(idx, 1); 610 | len--; 611 | this.value = len ? Helpers.toJson(this._value) : null; 612 | if (this._allow_multiple && this._$list) { 613 | var child = this._$list_group.find('li[data-file-idx="' + idx + '"]'); 614 | if (child.length) child.remove(); 615 | } 616 | this._remove_file_by_url(url); 617 | }, 618 | _remove_file_by_url: function(url) { 619 | if (!this.frm || !this.frm.attachments) 620 | this._remove_files(url, function(ret) { 621 | if (!cint(ret)) Helpers.error('Unable to remove the uploaded attachment ({0}).', [url]); 622 | }); 623 | else this.frm.attachments.remove_attachment_by_filename( 624 | url, Helpers.fnBind(function() { 625 | this.parse_validate_and_set_in_model(this.value); 626 | this.refresh(); 627 | this._form_save(); 628 | }, this) 629 | ); 630 | }, 631 | _setup_list: function() { 632 | if (this._$list) return; 633 | $(this.$value.children()[0]).children().each(function(i, el) { 634 | $(el).addClass('ba-hidden'); 635 | }); 636 | this._$list = $( 637 | '
    ' 638 | + '
    ' 639 | + '' 641 | + '
    ' 642 | + '
    ' 643 | ).appendTo(this.input_area); 644 | this._$list_group = this._$list.find('ul.list-group'); 645 | var me = this; 646 | this._$list_group.click('.ba-remove', function() { 647 | var $el = $(this); 648 | if (!$el.hasClass('ba-remove')) return; 649 | var $parent = $el.parents('.ba-attachment'); 650 | if (!$parent.length) return; 651 | var idx = $parent.attr('data-file-idx'); 652 | if (!idx || !/[0-9]+/.test('' + idx)) return; 653 | idx = cint(idx); 654 | if (idx >= 0) me._remove_file_by_idx(idx); 655 | }); 656 | }, 657 | _destroy_list: function() { 658 | if (this._$list) { 659 | this._$list.remove(); 660 | $(this.$value.children()[0]).children().each(function(i, el) { 661 | $(el).removeClass('ba-hidden'); 662 | }); 663 | } 664 | this._$list = this._$list_group = null; 665 | }, 666 | _update_input: function(value, dataurl) { 667 | value = value || this._value[this._value.length - 1]; 668 | this.$input.toggle(false); 669 | var file_url_parts = value.match(/^([^:]+),(.+):(.+)$/), 670 | filename = null; 671 | if (file_url_parts) { 672 | filename = file_url_parts[1]; 673 | dataurl = file_url_parts[2] + ':' + file_url_parts[3]; 674 | } 675 | if (!filename) filename = dataurl ? value : value.split('/').pop(); 676 | var $link = this.$value.toggle(true).find('.attached-file-link'); 677 | if (!this._allow_multiple) $link.html(filename).attr('href', dataurl || value); 678 | else { 679 | $link.html(this._value.length > 1 680 | ? this._value.length + ' ' + __('files uploaded') 681 | : filename 682 | ).attr('href', '#'); 683 | if (this._$list && this._$list.hasClass('ba-hidden')) 684 | this._$list.removeClass('ba-hidden'); 685 | } 686 | }, 687 | _reset_input: function() { 688 | this.dataurl = null; 689 | this.fileobj = null; 690 | this.set_input(null); 691 | this.parse_validate_and_set_in_model(null); 692 | this.refresh(); 693 | }, 694 | _reset_value: function() { 695 | this.value = null; 696 | this.$input.toggle(true); 697 | this.$value.toggle(false); 698 | Helpers.clear(this._value); 699 | if (!this._allow_multiple) return; 700 | Helpers.clear(this._files); 701 | if (this._$list) 702 | this._$list_group.children().each(function(i, el) { 703 | $(el).remove(); 704 | }); 705 | }, 706 | _form_save: function() { 707 | if (this._disable_auto_save) return; 708 | this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); 709 | } 710 | }); -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/controls/v13/attach_image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({ 10 | _setup_control: function() { 11 | this._images_only = true; 12 | this._super(); 13 | } 14 | }); -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/uploader/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import Helpers from './../utils'; 10 | import Filetype from './../filetypes'; 11 | 12 | 13 | frappe.ui.FileUploader = class FileUploader extends frappe.ui.FileUploader { 14 | constructor(opts) { 15 | opts = Helpers.isPlainObject(opts) ? opts : {}; 16 | let extra = opts.extra || {}; 17 | delete opts.extra; 18 | super(opts); 19 | if (this.uploader) this._override_uploader(opts, extra); 20 | } 21 | _override_uploader(opts, extra) { 22 | var up = this.uploader, 23 | me = this; 24 | up._extra_restrictions = extra; 25 | up.$watch('show_file_browser', function(show_file_browser) { 26 | if (!show_file_browser || !up.$refs.file_browser) return; 27 | me._override_file_browser( 28 | up.$refs.file_browser, 29 | !Helpers.isEmpty(opts.restrictions) 30 | ? opts.restrictions 31 | : { 32 | max_file_size: null, 33 | max_number_of_files: null, 34 | allowed_file_types: [], 35 | crop_image_aspect_ratio: null, 36 | }, 37 | extra 38 | ); 39 | }); 40 | if (!Helpers.isEmpty(opts.restrictions)) up.restrictions.as_public = !!opts.restrictions.as_public; 41 | up.dropfiles = function(e) { 42 | up.is_dragging = false; 43 | if (Helpers.isObject(e) && Helpers.isObject(e.dataTransfer)) 44 | up.add_files(e.dataTransfer.files); 45 | }; 46 | up.check_restrictions = function(file) { 47 | let max_file_size = up.restrictions.max_file_size, 48 | {allowed_file_types = [], allowed_filename} = up._extra_restrictions, 49 | is_correct_type = true, 50 | valid_file_size = true, 51 | valid_filename = true; 52 | if (!Helpers.isEmpty(allowed_file_types)) 53 | is_correct_type = allowed_file_types.some(function(type) { 54 | if (Helpers.isRegExp(type)) return file.type && type.test(file.type); 55 | if (type.includes('/')) return file.type && file.type === type; 56 | if (type[0] === '.') return (file.name || file.file_name).endsWith(type); 57 | return false; 58 | }); 59 | if (max_file_size && file.size != null && file.size) 60 | valid_file_size = file.size <= max_file_size; 61 | if (allowed_filename) { 62 | if (Helpers.isRegExp(allowed_filename)) { 63 | valid_filename = file.name.match(allowed_filename); 64 | } else if (!Helpers.isEmpty(allowed_filename)) { 65 | valid_filename = allowed_filename === file.name; 66 | } 67 | } 68 | if (!is_correct_type) { 69 | console.warn('File skipped because of invalid file type', file); 70 | frappe.show_alert({ 71 | message: __('File "{0}" was skipped because of invalid file type', [file.name]), 72 | indicator: 'orange' 73 | }); 74 | } 75 | if (!valid_file_size) { 76 | console.warn('File skipped because of invalid file size', file.size, file); 77 | frappe.show_alert({ 78 | message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), 79 | indicator: 'orange' 80 | }); 81 | } 82 | if (!valid_filename) { 83 | console.warn('File skipped because of invalid filename', file, allowed_filename); 84 | frappe.show_alert({ 85 | message: __('File "{0}" was skipped because of invalid filename', [file.name]), 86 | indicator: 'orange' 87 | }); 88 | } 89 | return is_correct_type && valid_file_size && valid_filename; 90 | }; 91 | up.prepare_files = function(file_array) { 92 | let is_single = Helpers.isPlainObject(file_array), 93 | files = is_single ? [file_array] : Array.from(file_array); 94 | files = files.map(function(f) { 95 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 96 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 97 | if (f.size == null) f.size = 0; 98 | return f; 99 | }); 100 | files = files.filter(up.check_restrictions); 101 | if (Helpers.isEmpty(files)) return !is_single ? [] : null; 102 | files = files.map(function(file) { 103 | let is_image = file.type.startsWith('image'), 104 | size_kb = file.size ? file.size / 1024 : 0; 105 | return { 106 | file_obj: file, 107 | cropper_file: file, 108 | crop_box_data: null, 109 | is_image, 110 | optimize: is_image && size_kb > 200 && !(file.type || '').includes('svg'), 111 | name: file.name, 112 | doc: null, 113 | progress: 0, 114 | total: 0, 115 | failed: false, 116 | request_succeeded: false, 117 | error_message: null, 118 | uploading: false, 119 | private: !up.restrictions.as_public, 120 | }; 121 | }); 122 | return !is_single ? files : files[0]; 123 | }; 124 | up.add_files = function(file_array) { 125 | let files = up.prepare_files(file_array), 126 | max_number_of_files = up.restrictions.max_number_of_files; 127 | if (max_number_of_files) { 128 | let uploaded = (up.files || []).length, 129 | total = uploaded + files.length; 130 | if (total > max_number_of_files) { 131 | let slice_index = max_number_of_files - uploaded - 1; 132 | files.slice(slice_index).forEach(function(file) { 133 | up.show_max_files_number_warning(file, up.doctype); 134 | }); 135 | files = files.slice(0, max_number_of_files); 136 | } 137 | } 138 | up.files = up.files.concat(files); 139 | if ( 140 | up.files.length === 1 && !up.allow_multiple 141 | && up.restrictions.crop_image_aspect_ratio != null 142 | && up.files[0].is_image 143 | && !up.files[0].file_obj.type.includes('svg') 144 | ) up.toggle_image_cropper(0); 145 | }; 146 | up.upload_via_web_link = function() { 147 | let file_url = up.$refs.web_link.url; 148 | if (!file_url) { 149 | Helpers.error('Invalid URL'); 150 | up.close_dialog = true; 151 | return Promise.reject(); 152 | } 153 | file_url = decodeURI(file_url); 154 | up.close_dialog = true; 155 | let file = up.prepare_files({file_url}); 156 | return file ? up.upload_file(file) : Promise.reject(); 157 | }; 158 | up.google_drive_callback = function(data) { 159 | if (data.action == google.picker.Action.PICKED) { 160 | let file = up.prepare_files({ 161 | file_url: data.docs[0].url, 162 | file_name: data.docs[0].name 163 | }); 164 | if (file) up.upload_file(file); 165 | } 166 | else if (data.action == google.picker.Action.CANCEL) 167 | cur_frm.attachments.new_attachment(); 168 | }; 169 | } 170 | _override_file_browser(fb, opts, extra) { 171 | fb._restrictions = opts; 172 | fb._extra_restrictions = extra; 173 | fb.check_restrictions = function(file) { 174 | if (file.is_folder) return true; 175 | let max_file_size = fb._restrictions.max_file_size, 176 | {allowed_file_types = [], allowed_filename} = fb._extra_restrictions, 177 | is_correct_type = true, 178 | valid_file_size = true, 179 | valid_filename = true; 180 | if (!Helpers.isEmpty(allowed_file_types)) 181 | is_correct_type = allowed_file_types.some(function(type) { 182 | if (Helpers.isRegExp(type)) return file.type && type.test(file.type); 183 | if (type.includes('/')) return file.type && file.type === type; 184 | if (type[0] === '.') return (file.name || file.file_name).endsWith(type); 185 | return false; 186 | }); 187 | if (max_file_size && file.size != null && file.size) 188 | valid_file_size = file.size <= max_file_size; 189 | if (allowed_filename) { 190 | if (Helpers.isRegExp(allowed_filename)) { 191 | valid_filename = file.name.match(allowed_filename); 192 | } else if (!Helpers.isEmpty(allowed_filename)) { 193 | valid_filename = allowed_filename === file.name; 194 | } 195 | } 196 | if (!is_correct_type) { 197 | console.warn('File skipped because of invalid file type', file); 198 | frappe.show_alert({ 199 | message: __('File "{0}" was skipped because of invalid file type', [file.name]), 200 | indicator: 'orange' 201 | }); 202 | } 203 | if (!valid_file_size) { 204 | console.warn('File skipped because of invalid file size', file.size, file); 205 | frappe.show_alert({ 206 | message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), 207 | indicator: 'orange' 208 | }); 209 | } 210 | if (!valid_filename) { 211 | console.warn('File skipped because of invalid filename', file, allowed_filename); 212 | frappe.show_alert({ 213 | message: __('File "{0}" was skipped because of invalid filename', [file.name]), 214 | indicator: 'orange' 215 | }); 216 | } 217 | return is_correct_type && valid_file_size && valid_filename; 218 | }; 219 | fb.get_files_in_folder = function(folder, start) { 220 | return frappe.call( 221 | 'frappe_better_attach_control.api.get_files_in_folder', 222 | { 223 | folder, 224 | start, 225 | page_length: fb.page_length 226 | } 227 | ).then(function(r) { 228 | let { files = [], has_more = false } = r.message || {}; 229 | if (!Helpers.isEmpty(files)) { 230 | files = files.map(function(f) { 231 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 232 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 233 | if (f.size == null) f.size = 0; 234 | return f; 235 | }); 236 | files = files.filter(fb.check_restrictions); 237 | files.sort(function(a, b) { 238 | if (a.is_folder && b.is_folder) { 239 | return a.modified < b.modified ? -1 : 1; 240 | } 241 | if (a.is_folder) return -1; 242 | if (b.is_folder) return 1; 243 | return 0; 244 | }); 245 | files = files.map(function(file) { 246 | return fb.make_file_node(file); 247 | }); 248 | } 249 | return {files, has_more}; 250 | }); 251 | }; 252 | fb.search_by_name = frappe.utils.debounce(function() { 253 | if (fb.search_text === '') { 254 | fb.node = fb.folder_node; 255 | return; 256 | } 257 | if (fb.search_text.length < 3) return; 258 | frappe.call( 259 | 'frappe_better_attach_control.api.get_files_by_search_text', 260 | {text: fb.search_text} 261 | ).then(function(r) { 262 | let files = r.message || []; 263 | if (!Helpers.isEmpty(files)) { 264 | files = files.map(function(f) { 265 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 266 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 267 | if (f.size == null) f.size = 0; 268 | return f; 269 | }); 270 | files = files.filter(fb.check_restrictions); 271 | if (!Helpers.isEmpty(files)) 272 | files = files.map(function(file) { 273 | return fb.make_file_node(file); 274 | }); 275 | } 276 | if (!fb.folder_node) fb.folder_node = fb.node; 277 | fb.node = { 278 | label: __('Search Results'), 279 | value: '', 280 | children: files, 281 | by_search: true, 282 | open: true, 283 | filtered: true 284 | }; 285 | }); 286 | }, 300); 287 | } 288 | }; -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/uploader/v12/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import Helpers from './../../utils'; 10 | import Filetype from './../../filetypes'; 11 | 12 | 13 | frappe.ui.FileUploader = class FileUploader extends frappe.ui.FileUploader { 14 | constructor(opts) { 15 | opts = Helpers.isPlainObject(opts) ? opts : {}; 16 | var extra = opts.extra || {}; 17 | delete opts.extra; 18 | super(opts); 19 | if (this.uploader) this._override_uploader(opts, extra); 20 | } 21 | _override_uploader(opts, extra) { 22 | var up = this.uploader, 23 | me = this; 24 | up._extra_restrictions = extra; 25 | up.$watch('show_file_browser', function(show_file_browser) { 26 | if (!show_file_browser || !up.$refs.file_browser) return; 27 | me._override_file_browser( 28 | up.$refs.file_browser, 29 | !Helpers.isEmpty(opts.restrictions) 30 | ? opts.restrictions 31 | : { 32 | max_file_size: null, 33 | max_number_of_files: null, 34 | allowed_file_types: [], 35 | }, 36 | extra 37 | ); 38 | }); 39 | if (!Helpers.isEmpty(opts.restrictions)) up.restrictions.as_public = !!opts.restrictions.as_public; 40 | up.dropfiles = function(e) { 41 | up.is_dragging = false; 42 | if (Helpers.isObject(e) && Helpers.isObject(e.dataTransfer)) 43 | up.add_files(e.dataTransfer.files); 44 | }; 45 | up.check_restrictions = function(file) { 46 | var max_file_size = up.restrictions.max_file_size, 47 | {allowed_file_types = [], allowed_filename} = up._extra_restrictions, 48 | is_correct_type = true, 49 | valid_file_size = true, 50 | valid_filename = true; 51 | if (!Helpers.isEmpty(allowed_file_types)) 52 | is_correct_type = allowed_file_types.some(function(type) { 53 | if (Helpers.isRegExp(type)) return file.type && type.test(file.type); 54 | if (type.includes('/')) return file.type && file.type === type; 55 | if (type[0] === '.') return (file.name || file.file_name).endsWith(type); 56 | return false; 57 | }); 58 | if (max_file_size && file.size != null && file.size) 59 | valid_file_size = file.size < max_file_size; 60 | if (allowed_filename) { 61 | if (Helpers.isRegExp(allowed_filename)) { 62 | valid_filename = file.name.match(allowed_filename); 63 | } else if (!Helpers.isEmpty(allowed_filename)) { 64 | valid_filename = allowed_filename === file.name; 65 | } 66 | } 67 | if (!is_correct_type) { 68 | console.warn('File skipped because of invalid file type', file); 69 | frappe.show_alert({ 70 | message: __('File "{0}" was skipped because of invalid file type', [file.name]), 71 | indicator: 'orange' 72 | }); 73 | } 74 | if (!valid_file_size) { 75 | console.warn('File skipped because of invalid file size', file.size, file); 76 | frappe.show_alert({ 77 | message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), 78 | indicator: 'orange' 79 | }); 80 | } 81 | if (!valid_filename) { 82 | console.warn('File skipped because of invalid filename', file, allowed_filename); 83 | frappe.show_alert({ 84 | message: __('File "{0}" was skipped because of invalid filename', [file.name]), 85 | indicator: 'orange' 86 | }); 87 | } 88 | return is_correct_type && valid_file_size && valid_filename; 89 | }; 90 | up.show_max_files_number_warning = function(file, max_number_of_files) { 91 | console.warn( 92 | 'File skipped because it exceeds the allowed specified limit of ' + max_number_of_files + ' uploads', 93 | file 94 | ); 95 | var MSG; 96 | if (up.doctype) { 97 | MSG = __('File "{0}" was skipped because only {1} uploads are allowed for DocType "{2}"', 98 | [file.name, max_number_of_files, up.doctype]); 99 | } else { 100 | MSG = __('File "{0}" was skipped because only {1} uploads are allowed', 101 | [file.name, max_number_of_files]); 102 | } 103 | frappe.show_alert({ 104 | message: MSG, 105 | indicator: "orange", 106 | }); 107 | }; 108 | up.prepare_files = function(file_array) { 109 | var is_single = Helpers.isPlainObject(file_array), 110 | files = is_single ? [file_array] : Array.from(file_array); 111 | files = files.map(function(f) { 112 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 113 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 114 | if (f.size == null) f.size = 0; 115 | return f; 116 | }); 117 | files = files.filter(up.check_restrictions); 118 | if (Helpers.isEmpty(files)) return !is_single ? [] : null; 119 | files = files.map(function(file) { 120 | var is_image = file.type.startsWith('image'); 121 | return { 122 | file_obj: file, 123 | is_image, 124 | name: file.name, 125 | doc: null, 126 | progress: 0, 127 | total: 0, 128 | failed: false, 129 | uploading: false, 130 | private: !up.restrictions.as_public || !is_image, 131 | }; 132 | }); 133 | return !is_single ? files : files[0]; 134 | }; 135 | up.add_files = function(file_array) { 136 | var files = up.prepare_files(file_array), 137 | max_number_of_files = up.restrictions.max_number_of_files; 138 | if (max_number_of_files) { 139 | var uploaded = (up.files || []).length, 140 | total = uploaded + files.length; 141 | if (total > max_number_of_files) { 142 | var slice_index = max_number_of_files - uploaded - 1; 143 | files.slice(slice_index).forEach(function(file) { 144 | up.show_max_files_number_warning(file, max_number_of_files); 145 | }); 146 | files = files.slice(0, max_number_of_files); 147 | } 148 | } 149 | up.files = up.files.concat(files); 150 | }; 151 | up.upload_via_web_link = function() { 152 | var file_url = up.$refs.web_link.url; 153 | if (!file_url) { 154 | Helpers.error('Invalid URL'); 155 | return Promise.reject(); 156 | } 157 | file_url = decodeURI(file_url); 158 | var file = up.prepare_files({file_url}); 159 | return file ? up.upload_file(file) : Promise.reject(); 160 | }; 161 | } 162 | _override_file_browser(fb, opts, extra) { 163 | fb._restrictions = opts; 164 | fb._extra_restrictions = extra; 165 | fb.check_restrictions = function(file) { 166 | if (file.is_folder) return true; 167 | var max_file_size = fb._restrictions.max_file_size, 168 | {allowed_file_types = [], allowed_filename} = fb._extra_restrictions, 169 | is_correct_type = true, 170 | valid_file_size = true, 171 | valid_filename = true; 172 | if (!Helpers.isEmpty(allowed_file_types)) 173 | is_correct_type = allowed_file_types.some(function(type) { 174 | if (Helpers.isRegExp(type)) return file.type && type.test(file.type); 175 | if (type.includes('/')) return file.type && file.type === type; 176 | if (type[0] === '.') return (file.name || file.file_name).endsWith(type); 177 | return false; 178 | }); 179 | if (max_file_size && file.size != null && file.size) 180 | valid_file_size = file.size < max_file_size; 181 | if (allowed_filename) { 182 | if (Helpers.isRegExp(allowed_filename)) { 183 | valid_filename = file.name.match(allowed_filename); 184 | } else if (!Helpers.isEmpty(allowed_filename)) { 185 | valid_filename = allowed_filename === file.name; 186 | } 187 | } 188 | if (!is_correct_type) { 189 | console.warn('File skipped because of invalid file type', file); 190 | frappe.show_alert({ 191 | message: __('File "{0}" was skipped because of invalid file type', [file.name]), 192 | indicator: 'orange' 193 | }); 194 | } 195 | if (!valid_file_size) { 196 | console.warn('File skipped because of invalid file size', file.size, file); 197 | frappe.show_alert({ 198 | message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), 199 | indicator: 'orange' 200 | }); 201 | } 202 | if (!valid_filename) { 203 | console.warn('File skipped because of invalid filename', file, allowed_filename); 204 | frappe.show_alert({ 205 | message: __('File "{0}" was skipped because of invalid filename', [file.name]), 206 | indicator: 'orange' 207 | }); 208 | } 209 | return is_correct_type && valid_file_size && valid_filename; 210 | }; 211 | fb.get_files_in_folder = function(folder) { 212 | return frappe.call( 213 | 'frappe_better_attach_control.api.get_files_in_folder', 214 | {folder} 215 | ).then(function(r) { 216 | var files = r.message || []; 217 | if (!Helpers.isEmpty(files)) { 218 | files = files.map(function(f) { 219 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 220 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 221 | if (f.size == null) f.size = 0; 222 | return f; 223 | }); 224 | files = files.filter(fb.check_restrictions); 225 | files.sort(function(a, b) { 226 | if (a.is_folder && b.is_folder) { 227 | return a.modified < b.modified ? -1 : 1; 228 | } 229 | if (a.is_folder) return -1; 230 | if (b.is_folder) return 1; 231 | return 0; 232 | }); 233 | files = files.map(function(file) { 234 | var filename = file.file_name || file.name; 235 | return { 236 | label: frappe.utils.file_name_ellipsis(filename, 40), 237 | filename: filename, 238 | file_url: file.file_url, 239 | value: file.name, 240 | is_leaf: !file.is_folder, 241 | fetched: !file.is_folder, 242 | children: [], 243 | open: false, 244 | fetching: false, 245 | filtered: true 246 | }; 247 | }); 248 | } 249 | return files; 250 | }); 251 | }; 252 | } 253 | }; -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/uploader/v13/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | import Helpers from './../../utils'; 10 | import Filetype from './../../filetypes'; 11 | 12 | 13 | frappe.ui.FileUploader = class FileUploader extends frappe.ui.FileUploader { 14 | constructor(opts) { 15 | opts = Helpers.isPlainObject(opts) ? opts : {}; 16 | var extra = opts.extra || {}; 17 | delete opts.extra; 18 | super(opts); 19 | if (this.uploader) this._override_uploader(opts, extra); 20 | } 21 | _override_uploader(opts, extra) { 22 | var up = this.uploader, 23 | me = this; 24 | up._extra_restrictions = extra; 25 | up.$watch('show_file_browser', function(show_file_browser) { 26 | if (!show_file_browser || !up.$refs.file_browser) return; 27 | me._override_file_browser( 28 | up.$refs.file_browser, 29 | !Helpers.isEmpty(opts.restrictions) 30 | ? opts.restrictions 31 | : { 32 | max_file_size: null, 33 | max_number_of_files: null, 34 | allowed_file_types: [], 35 | }, 36 | extra 37 | ); 38 | }); 39 | if (!Helpers.isEmpty(opts.restrictions)) up.restrictions.as_public = !!opts.restrictions.as_public; 40 | up.dropfiles = function(e) { 41 | up.is_dragging = false; 42 | if (Helpers.isObject(e) && Helpers.isObject(e.dataTransfer)) 43 | up.add_files(e.dataTransfer.files); 44 | }; 45 | up.check_restrictions = function(file) { 46 | var max_file_size = up.restrictions.max_file_size, 47 | {allowed_file_types = [], allowed_filename} = up._extra_restrictions, 48 | is_correct_type = true, 49 | valid_file_size = true, 50 | valid_filename = true; 51 | if (!Helpers.isEmpty(allowed_file_types)) 52 | is_correct_type = allowed_file_types.some(function(type) { 53 | if (Helpers.isRegExp(type)) return file.type && type.test(file.type); 54 | if (type.includes('/')) return file.type && file.type === type; 55 | if (type[0] === '.') return (file.name || file.file_name).endsWith(type); 56 | return false; 57 | }); 58 | if (max_file_size && file.size != null && file.size) 59 | valid_file_size = file.size < max_file_size; 60 | if (allowed_filename) { 61 | if (Helpers.isRegExp(allowed_filename)) { 62 | valid_filename = file.name.match(allowed_filename); 63 | } else if (!Helpers.isEmpty(allowed_filename)) { 64 | valid_filename = allowed_filename === file.name; 65 | } 66 | } 67 | if (!is_correct_type) { 68 | console.warn('File skipped because of invalid file type', file); 69 | frappe.show_alert({ 70 | message: __('File "{0}" was skipped because of invalid file type', [file.name]), 71 | indicator: 'orange' 72 | }); 73 | } 74 | if (!valid_file_size) { 75 | console.warn('File skipped because of invalid file size', file.size, file); 76 | frappe.show_alert({ 77 | message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), 78 | indicator: 'orange' 79 | }); 80 | } 81 | if (!valid_filename) { 82 | console.warn('File skipped because of invalid filename', file, allowed_filename); 83 | frappe.show_alert({ 84 | message: __('File "{0}" was skipped because of invalid filename', [file.name]), 85 | indicator: 'orange' 86 | }); 87 | } 88 | return is_correct_type && valid_file_size && valid_filename; 89 | }; 90 | up.show_max_files_number_warning = function(file, max_number_of_files) { 91 | console.warn( 92 | 'File skipped because it exceeds the allowed specified limit of ' + max_number_of_files + ' uploads', 93 | file 94 | ); 95 | var MSG; 96 | if (up.doctype) { 97 | MSG = __('File "{0}" was skipped because only {1} uploads are allowed for DocType "{2}"', 98 | [file.name, max_number_of_files, up.doctype]); 99 | } else { 100 | MSG = __('File "{0}" was skipped because only {1} uploads are allowed', 101 | [file.name, max_number_of_files]); 102 | } 103 | frappe.show_alert({ 104 | message: MSG, 105 | indicator: "orange", 106 | }); 107 | }; 108 | up.prepare_files = function(file_array) { 109 | var is_single = Helpers.isPlainObject(file_array), 110 | files = is_single ? [file_array] : Array.from(file_array); 111 | files = files.map(function(f) { 112 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 113 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 114 | if (f.size == null) f.size = 0; 115 | return f; 116 | }); 117 | files = files.filter(up.check_restrictions); 118 | if (Helpers.isEmpty(files)) return !is_single ? [] : null; 119 | files = files.map(function(file) { 120 | var is_image = file.type.startsWith('image'); 121 | return { 122 | file_obj: file, 123 | is_image, 124 | name: file.name, 125 | doc: null, 126 | progress: 0, 127 | total: 0, 128 | failed: false, 129 | uploading: false, 130 | private: !up.restrictions.as_public || !is_image, 131 | }; 132 | }); 133 | return !is_single ? files : files[0]; 134 | }; 135 | up.add_files = function(file_array, custom) { 136 | var files = up.prepare_files(file_array), 137 | max_number_of_files = up.restrictions.max_number_of_files; 138 | if (max_number_of_files) { 139 | var uploaded = (up.files || []).length, 140 | total = uploaded + files.length; 141 | if (total > max_number_of_files) { 142 | var slice_index = max_number_of_files - uploaded - 1; 143 | files.slice(slice_index).forEach(function(file) { 144 | up.show_max_files_number_warning(file, max_number_of_files); 145 | }); 146 | files = files.slice(0, max_number_of_files); 147 | } 148 | } 149 | up.files = up.files.concat(files); 150 | }; 151 | up.upload_via_web_link = function() { 152 | var file_url = up.$refs.web_link.url; 153 | if (!file_url) { 154 | Helpers.error('Invalid URL'); 155 | return Promise.reject(); 156 | } 157 | file_url = decodeURI(file_url); 158 | var file = up.prepare_files({file_url}); 159 | return file ? up.upload_file(file) : Promise.reject(); 160 | }; 161 | } 162 | _override_file_browser(fb, opts, extra) { 163 | fb._restrictions = opts; 164 | fb._extra_restrictions = extra; 165 | fb.check_restrictions = function(file) { 166 | if (file.is_folder) return true; 167 | var max_file_size = fb._restrictions.max_file_size, 168 | {allowed_file_types = [], allowed_filename} = fb._extra_restrictions, 169 | is_correct_type = true, 170 | valid_file_size = true, 171 | valid_filename = true; 172 | if (!Helpers.isEmpty(allowed_file_types)) 173 | is_correct_type = allowed_file_types.some(function(type) { 174 | if (Helpers.isRegExp(type)) return file.type && type.test(file.type); 175 | if (type.includes('/')) return file.type && file.type === type; 176 | if (type[0] === '.') return (file.name || file.file_name).endsWith(type); 177 | return false; 178 | }); 179 | if (max_file_size && file.size != null && file.size) 180 | valid_file_size = file.size < max_file_size; 181 | if (allowed_filename) { 182 | if (Helpers.isRegExp(allowed_filename)) { 183 | valid_filename = file.name.match(allowed_filename); 184 | } else if (!Helpers.isEmpty(allowed_filename)) { 185 | valid_filename = allowed_filename === file.name; 186 | } 187 | } 188 | if (!is_correct_type) { 189 | console.warn('File skipped because of invalid file type', file); 190 | frappe.show_alert({ 191 | message: __('File "{0}" was skipped because of invalid file type', [file.name]), 192 | indicator: 'orange' 193 | }); 194 | } 195 | if (!valid_file_size) { 196 | console.warn('File skipped because of invalid file size', file.size, file); 197 | frappe.show_alert({ 198 | message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]), 199 | indicator: 'orange' 200 | }); 201 | } 202 | if (!valid_filename) { 203 | console.warn('File skipped because of invalid filename', file, allowed_filename); 204 | frappe.show_alert({ 205 | message: __('File "{0}" was skipped because of invalid filename', [file.name]), 206 | indicator: 'orange' 207 | }); 208 | } 209 | return is_correct_type && valid_file_size && valid_filename; 210 | }; 211 | fb.get_files_in_folder = function(folder, start) { 212 | return frappe.call( 213 | 'frappe_better_attach_control.api.get_files_in_folder', 214 | { 215 | folder, 216 | start, 217 | page_length: fb.page_length 218 | } 219 | ).then(function(r) { 220 | var { files = [], has_more = false } = r.message || {}; 221 | if (!Helpers.isEmpty(files)) { 222 | files = files.map(function(f) { 223 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 224 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 225 | if (f.size == null) f.size = 0; 226 | return f; 227 | }); 228 | files = files.filter(fb.check_restrictions); 229 | files.sort(function(a, b) { 230 | if (a.is_folder && b.is_folder) { 231 | return a.modified < b.modified ? -1 : 1; 232 | } 233 | if (a.is_folder) return -1; 234 | if (b.is_folder) return 1; 235 | return 0; 236 | }); 237 | files = files.map(function(file) { 238 | return fb.make_file_node(file); 239 | }); 240 | } 241 | return { files, has_more }; 242 | }); 243 | }; 244 | fb.search_by_name = frappe.utils.debounce(function() { 245 | if (fb.search_text === '') { 246 | fb.node = fb.folder_node; 247 | return; 248 | } 249 | if (fb.search_text.length < 3) return; 250 | frappe.call( 251 | 'frappe_better_attach_control.api.get_files_by_search_text', 252 | {text: fb.search_text} 253 | ).then(function(r) { 254 | var files = r.message || []; 255 | if (!Helpers.isEmpty(files)) { 256 | files = files.map(function(f) { 257 | if (f.name == null) f.name = f.file_name || Filetype.get_filename(f.file_url); 258 | if (f.type == null) f.type = Filetype.get_file_type(Filetype.get_file_ext(f.file_url)) || ''; 259 | if (f.size == null) f.size = 0; 260 | return f; 261 | }); 262 | files = files.filter(fb.check_restrictions) 263 | .map(function(file) { 264 | return fb.make_file_node(file); 265 | }); 266 | } 267 | if (!fb.folder_node) fb.folder_node = fb.node; 268 | fb.node = { 269 | label: __('Search Results'), 270 | value: '', 271 | children: files, 272 | by_search: true, 273 | open: true, 274 | filtered: true 275 | }; 276 | }); 277 | }, 300); 278 | } 279 | }; -------------------------------------------------------------------------------- /frappe_better_attach_control/public/js/utils/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Frappe Better Attach Control © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | var Helpers = { 10 | $type: function(v) { 11 | if (v == null) return v === void 0 ? 'Undefined' : 'Null'; 12 | var t = Object.prototype.toString.call(v).slice(8, -1); 13 | return t === 'Number' && isNaN(v) ? 'NaN' : t; 14 | }, 15 | $of: function(v, t) { return this.$type(v) === t; }, 16 | $ofAny: function(v, t) { return t.split(' ').indexOf(this.$type(v)) >= 0; }, 17 | $propOf: function(v, k) { return Object.prototype.hasOwnProperty.call(v, k); }, 18 | $fnStr: function(v) { return Function.prototype.toString.call(v); }, 19 | 20 | // Common Checks 21 | isString: function(v) { return this.$of(v, 'String'); }, 22 | isObjectLike: function(v) { return v != null && typeof v === 'object'; }, 23 | isNumber: function(v) { return this.$of(v, 'Number') && !isNaN(v); }, 24 | isLength: function(v) { 25 | return this.isNumber(v) && v >= 0 && v % 1 == 0 && v <= 9007199254740991; 26 | }, 27 | isInteger: function(v) { return this.isNumber(v) && v === Number(parseInt(v)); }, 28 | isFunction: function(v) { return typeof v === 'function' || /(Function|^Proxy)$/.test(this.$type(v)); }, 29 | isArrayLike: function(v) { 30 | return this.isObjectLike(v) && !this.isFunction(v) && !this.$ofAny(v, 'String Window') 31 | && v !== window && !/^(NodeList|HTML(\w+|)Collection)$/.test(this.$type(v)) 32 | && !this.isInteger(v.nodeType) && this.isLength(v.length); 33 | }, 34 | 35 | // Checks 36 | isArray: function(v) { return this.$of(v, 'Array'); }, 37 | isObject: function(v) { 38 | return this.isObjectLike(v) 39 | && !this.$ofAny(v, 'String Number Boolean Array RegExp Date URL') 40 | && this.isObjectLike(Object.getPrototypeOf(v)); 41 | }, 42 | isPlainObject: function(v) { 43 | if (!this.isObject(v)) return false; 44 | let k = 'constructor'; v = Object.getPrototypeOf(v); 45 | return v && this.$propOf(v, k) && this.isFunction(v[k]) 46 | && this.$fnStr(v[k]) === this.$fnStr(Object); 47 | }, 48 | isIteratable: function(v) { return this.isArrayLike(v) || this.isObject(v); }, 49 | isEmpty: function(v) { 50 | if (v == null) return true; 51 | if (this.isString(v) || this.isArrayLike(v)) return !v.length; 52 | if (this.isObject(v)) { 53 | for (var k in v) if (this.$propOf(v, k)) return false; 54 | } 55 | return !v; 56 | }, 57 | isJson: function(v) { return this.parseJson(v, null) !== null; }, 58 | isRegExp: function(v) { return this.$of(v, 'RegExp'); }, 59 | 60 | // Json 61 | parseJson: function(v, d) { 62 | try { return JSON.parse(v); } catch(_) {} 63 | return d === void 0 ? v : d; 64 | }, 65 | toJson: function(v) { 66 | try { return JSON.stringify(v); } catch(_) {} 67 | return ''; 68 | }, 69 | 70 | // Control 71 | ifNull: function(v, d) { return v != null ? v : d; }, 72 | 73 | // Data 74 | each: function(data, fn, bind) { 75 | fn = this.fnBind(fn, bind || this); 76 | if (this.isArrayLike(data)) { 77 | for (var i = 0, l = data.length; i < l; i++) { 78 | if (fn(data[i], i) === false) return; 79 | } 80 | } else if (this.isObject(data)) { 81 | for (var k in data) { 82 | if (fn(data[k], k) === false) return; 83 | } 84 | } 85 | }, 86 | filter: function(data, fn, bind) { 87 | if (!this.isIteratable(data)) return; 88 | fn = this.fnBind(fn, bind || this); 89 | var arr = this.isArrayLike(data), 90 | ret = arr ? [] : {}; 91 | this.each(data, function(v, k) { 92 | if (fn(v, k) !== false) arr ? ret.push(v) : (ret[k] = v); 93 | }); 94 | return ret; 95 | }, 96 | map: function(data, fn, bind) { 97 | if (!this.isIteratable(data)) return; 98 | fn = this.fnBind(fn, bind || this); 99 | this.each(data, function(v, k) { data[k] = fn(v, k); }); 100 | return data; 101 | }, 102 | clear: function(d) { 103 | if (this.isArray(d)) d.splice(0, d.length); 104 | else if (this.isObject(d)) { 105 | for (var k in d) { 106 | if (this.$propOf(d, k)) delete d[k]; 107 | } 108 | } 109 | return d; 110 | }, 111 | deepClone: function(v) { 112 | if (!this.isIteratable(v)) return v; 113 | var ret = this.toJson(v); 114 | if (ret.length) { 115 | ret = this.parseJson(ret, null); 116 | if (this.isIteratable(ret)) return ret; 117 | } 118 | var arr = this.isArrayLike(v); 119 | ret = arr ? [] : {}; 120 | this.each(v, function(y, x) { 121 | if (this.isIteratable(y)) y = this.deepClone(y); 122 | arr ? ret.push(y) : (ret[x] = y); 123 | }); 124 | return ret; 125 | }, 126 | merge: function(b) { 127 | if (!this.isIteratable(b)) return arguments[1] || null; 128 | b = this.deepClone(b); 129 | this.each(arguments, function(a, i) { 130 | i && this.each(a, function(y, x) { 131 | if (this.isObject(y) && this.isObject(b[x])) y = this.merge(b[x], y); 132 | if (!this.isArrayLike(b[x])) b[x] = y; 133 | else if (!this.isArrayLike(y)) Array.prototype.push.call(b[x], y); 134 | else Array.prototype.push.apply(b[x], y); 135 | }); 136 | }); 137 | return b; 138 | }, 139 | isEqual: function(data, base) { 140 | if (this.isIteratable(data) !== this.isIteratable(base)) return data == base; 141 | if (this.isEmpty(data) && this.isEmpty(base)) return this.$type(data) === this.$type(base); 142 | var ret = true; 143 | this.each(data, function(v, k) { 144 | if (!this.isEqual(v, base[k])) return (ret = false); 145 | }); 146 | return ret; 147 | }, 148 | 149 | // Converter 150 | toBool: function(v) { return [true, 'true', 1, '1'].indexOf(v) >= 0; }, 151 | toArray: function(v, def) { 152 | if (def === void 0) def = []; 153 | if (v == null) return def; 154 | if (this.isArray(v)) return v; 155 | if (this.isObject(v)) return Object.values(v); 156 | if (!this.isJson(v)) return [v]; 157 | v = this.parseJson(v, null); 158 | return this.isArray(v) ? v : def; 159 | }, 160 | 161 | // Function 162 | fnCall: function(fn, a, o) { 163 | if (!this.isFunction(fn)) return; 164 | a = this.toArray(a); 165 | o = o || this; 166 | switch (a.length) { 167 | case 0: return fn.call(o); 168 | case 1: return fn.call(o, a[0]); 169 | case 2: return fn.call(o, a[0], a[1]); 170 | case 3: return fn.call(o, a[0], a[1], a[2]); 171 | case 4: return fn.call(o, a[0], a[1], a[2], a[3]); 172 | case 5: return fn.call(o, a[0], a[1], a[2], a[3], a[4]); 173 | default: return fn.apply(o, a); 174 | } 175 | }, 176 | fnBind: function(f, b) { 177 | if (!this.isFunction(f)) return; 178 | var me = this; 179 | return function() { return me.fnCall(f, arguments, b); }; 180 | }, 181 | 182 | // Format 183 | FILE_SIZES: ['B', 'KB', 'MB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 184 | formatSize: function(v) { 185 | v = parseFloat(v); 186 | if (!v) return '0 ' + this.FILE_SIZES[0]; 187 | var k = 1024, 188 | i = Math.floor(Math.log(v) / Math.log(k)), 189 | t = v / Math.pow(k, i); 190 | if ((k - t) < 1) { 191 | i++; 192 | t = v / Math.pow(k, i); 193 | } 194 | return flt(t, 2, '#,###.##') + ' ' + this.FILE_SIZES[i]; 195 | }, 196 | 197 | // Error 198 | log: function() { 199 | var pre = '[Better Attach]: '; 200 | this.each(arguments, function(v) { 201 | if (this.isString(v)) console.log(pre + v); 202 | else console.log(pre, v); 203 | }); 204 | }, 205 | elog: function() { 206 | var pre = '[Better Attach]: '; 207 | this.each(arguments, function(v) { 208 | if (this.isString(v)) console.error(pre + v); 209 | else console.error(pre, v); 210 | }); 211 | }, 212 | error: function(text, args, _throw) { 213 | if (_throw == null && args === true) { 214 | _throw = args; 215 | args = null; 216 | } 217 | text = '[Better Attach]: ' + text; 218 | if (_throw) { 219 | frappe.throw(__(text, args)); 220 | return; 221 | } 222 | frappe.msgprint({ 223 | title: __('Error'), 224 | indicator: 'Red', 225 | message: __(text, args), 226 | }); 227 | }, 228 | 229 | // Call 230 | request: function(method, args, success, failed, always) { 231 | if (args && this.isFunction(args)) { 232 | if (this.isFunction(failed)) always = failed; 233 | if (this.isFunction(success)) failed = success; 234 | success = args; 235 | args = null; 236 | } 237 | var data = {type: 'GET'}; 238 | if (args != null) { 239 | data.type = 'POST'; 240 | if (!this.isPlainObject(args)) data.args = {'data': args}; 241 | else { 242 | data.args = args; 243 | if (args.type && args.args) { 244 | data.type = args.type; 245 | data.args = args.args; 246 | } 247 | } 248 | } 249 | if (this.isString(method)) { 250 | data.method = 'frappe_better_attach_control.api.' + method; 251 | } else if (this.isArray(method)) { 252 | data.doc = method[0]; 253 | data.method = method[1]; 254 | } else { 255 | this.elog('The method passed is invalid', arguments); 256 | return; 257 | } 258 | data.error = this.fnBind(function(e) { 259 | this.fnCall(failed); 260 | this.elog('Call error.', e); 261 | this.error('Unable to make the call to {0}', [data.method]); 262 | }); 263 | if (this.isFunction(success)) { 264 | data.callback = this.fnBind(function(ret) { 265 | if (ret && this.isPlainObject(ret)) ret = ret.message || ret; 266 | try { 267 | this.fnCall(success, ret); 268 | } catch(e) { this.error(e); } 269 | }); 270 | } 271 | if (this.isFunction(always)) data.always = always; 272 | try { 273 | return frappe.call(data); 274 | } catch(e) { 275 | this.error(e); 276 | } 277 | } 278 | }; 279 | 280 | 281 | export default Helpers; -------------------------------------------------------------------------------- /frappe_better_attach_control/version.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import __version__ 8 | 9 | 10 | # [Internal] 11 | __frappe_version__ = int(__version__.split(".")[0]) 12 | 13 | 14 | # [Hooks, Attachment] 15 | def is_version_gt(num: int): 16 | return __frappe_version__ > num 17 | 18 | 19 | # [Common] 20 | def is_version_lt(num: int): 21 | return __frappe_version__ < num -------------------------------------------------------------------------------- /images/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kid1194/frappe-better-attach-control/63e3cdbdddac89b33cc7c101efe717d698a72140/images/screenshot_1.png -------------------------------------------------------------------------------- /images/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kid1194/frappe-better-attach-control/63e3cdbdddac89b33cc7c101efe717d698a72140/images/screenshot_2.png -------------------------------------------------------------------------------- /images/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kid1194/frappe-better-attach-control/63e3cdbdddac89b33cc7c101efe717d698a72140/images/screenshot_3.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "frappe_better_attach_control" 3 | authors = [ 4 | {name = "Ameen Ahmed (Level Up)", email = "kid1194@gmail.com"} 5 | ] 6 | description = "Frappe attach control that supports customization." 7 | keywords = ["frappe", "attach", "better attach"] 8 | classifiers = [ 9 | "Development Status :: 4 - Beta", 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: JavaScript" 12 | ] 13 | requires-python = ">=3.6" 14 | readme = "README.md" 15 | dynamic = ["version"] 16 | dependencies = [ 17 | "frappe>=12.0.0" 18 | ] 19 | 20 | [project.urls] 21 | Documentation = "https://github.com/kid1194/frappe-better-attach-control" 22 | Source = "https://github.com/kid1194/frappe-better-attach-control" 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | frappe>=12.0.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Frappe Better Attach Control © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from setuptools import setup, find_packages 8 | from frappe_better_attach_control import __version__ as version 9 | 10 | 11 | with open('requirements.txt') as f: 12 | install_requires = f.read().strip().split('\n') 13 | 14 | 15 | setup( 16 | name='frappe_better_attach_control', 17 | version=version, 18 | description='Frappe attach control that supports customization.', 19 | author='Ameen Ahmed (Level Up)', 20 | author_email='kid1194@gmail.com', 21 | packages=find_packages(), 22 | zip_safe=False, 23 | include_package_data=True, 24 | install_requires=install_requires 25 | ) --------------------------------------------------------------------------------