├── .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 | 
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 |
17 |
18 |
19 |
20 |
21 |
22 |
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 | - [](https://github.com/mohsinalimat)
37 | - [](https://github.com/robert1112)
38 | - [](https://github.com/NirajRegmi)
39 | - [](https://github.com/galaxlabs)
40 | #### Version 1
41 | - [](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 ''
449 | + ' '
450 | + __("Your browser does not support the video element.")
451 | + ' ';
452 | }
453 | if (file.class === 'audio') {
454 | return ''
455 | + ' '
456 | + __("Your browser does not support the audio element.")
457 | + ' ';
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 | + '
'
600 | + '
'
601 | + ''
602 | + ' '
603 | + ' '
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 | ''
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 ''
416 | + ' '
417 | + __("Your browser does not support the video element.")
418 | + ' ';
419 | }
420 | if (file.class === 'audio') {
421 | return ''
422 | + ' '
423 | + __("Your browser does not support the audio element.")
424 | + ' ';
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 | + '
'
568 | + '
'
569 | + ''
570 | + ' '
571 | + ' '
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 | ''
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 ''
435 | + ' '
436 | + __("Your browser does not support the video element.")
437 | + ' ';
438 | }
439 | if (file.class === 'audio') {
440 | return ''
441 | + ' '
442 | + __("Your browser does not support the audio element.")
443 | + ' ';
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 | + '
'
587 | + '
'
588 | + ''
589 | + ' '
590 | + ' '
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 | ''
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 | )
--------------------------------------------------------------------------------