@ -0,0 +1,2 @@ |
|||
# -*- coding: utf-8 -*- |
|||
from . import models |
@ -0,0 +1,100 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################# |
|||
# Author : Expert IT Solutions (<www.expertpk.com>) |
|||
# Copyright(c): 2012-Present Expert IT Solutions |
|||
# All Rights Reserved. |
|||
# |
|||
# This program is copyright property of the author mentioned above. |
|||
# You can`t redistribute it and/or modify it. |
|||
# |
|||
################################################################################# |
|||
{ |
|||
'name': "One2many Image Drag and Drop Widget | Image Drag And Drop Widget ", |
|||
'version': '17.0.1.4.1', |
|||
'author': "Expert IT Solutions", |
|||
'website': "https://www.expertpk.com", |
|||
'summary': """ |
|||
Two widgets. 1: Binary Image Drag and Drop Widget 2: One2Many Image(s) Field Drag And Drop Widget. |
|||
Enhance your Odoo experience with a custom widget for drag and drop image functionality. |
|||
Keywords: one2many image drag and drop, image drag and drop, drag & drop, binary field image upload, file uploader, bulk image upload, media management, image preview, product image uploader, one2many drag and drop, o2m drag and drop, binary drag & drop, drag and drop pictures, image widget, zoom image, image zoom, binary fields, image_ids, product_template_image_ids, product_variant_image_ids, multiple images, upload multiple images, one2many widget, o2m widget, o2m uploader upload uploading image images multi media multimedia media owl widget widget view custom widget field widget. |
|||
""", |
|||
'description': """ |
|||
The Drag And Drop Binary Field Widget for Odoo is a cutting-edge proprietary solution designed for advanced media management. |
|||
It provides an intuitive drag and drop interface to upload, preview, and manage images efficiently within your Odoo records. |
|||
|
|||
Key Features and Keywords: |
|||
- Image Drag And Drop Widget |
|||
- Bulk Image Upload |
|||
- Media Management |
|||
- File Uploader |
|||
- Odoo Binary Field Widget |
|||
- Custom Odoo Widget |
|||
- E-commerce Image Management |
|||
- Real Estate Image Management |
|||
- Drag and drop widget for Odoo |
|||
- Odoo image upload |
|||
- Odoo image preview |
|||
- Odoo file uploader |
|||
- Drag and drop binary field |
|||
- o2m field widget |
|||
- one2many field widget |
|||
- one2many drag and drop |
|||
- one2many image upload |
|||
- multiple image upload |
|||
- multiple images drag and drop |
|||
- drag and drop multiple images |
|||
- drag and drop multiple files |
|||
- image_1920 field widget |
|||
- image.mixin field widget |
|||
- product.image field widget |
|||
- website product multiple images |
|||
- website product drag and drop widget |
|||
- website product image upload |
|||
- Drag and Drop Interface |
|||
- Binary Field Widget |
|||
- Image Upload & Preview |
|||
- File Uploader & Media Management |
|||
- Custom Odoo Widget for E-commerce, Real Estate, Media Portals, and More |
|||
- Seamless Integration with Odoo's Backend |
|||
- Product Image Uploader, Custom Image Fields, and Dynamic Widgets |
|||
|
|||
IMPORTANT LICENSE & USAGE TERMS: |
|||
- Licensed under Odoo Proprietary License v1 (OPL-1). This license is extremely restrictive. |
|||
- Modification, redistribution, or reuse of any part of this module’s code in any other modules or applications is strictly prohibited. |
|||
- This module is intended for use on a single database only. For additional databases, buy additional licenses. |
|||
- Code modifications, reverse-engineering, or using the code to generate derivative works is not allowed. |
|||
- For full details, please refer to the separate LICENSE file provided with the module. |
|||
|
|||
Perfect for: |
|||
- E-commerce websites looking to manage multiple product images effortlessly. |
|||
- Businesses with heavy media content, such as real estate, portfolios, or media portals. |
|||
- Developers and system integrators seeking to enhance the user experience without extra dependencies, |
|||
as the widget is available systemwide once installed. |
|||
""", |
|||
'license': 'OPL-1', |
|||
'category': 'Tools', |
|||
'depends': ['base_setup', 'base', 'web', 'product', 'website_sale'], |
|||
# uncomment above line to automate the process of product upgrade and comment the below depend line |
|||
# 'depends': ['base_setup', 'base', 'web', 'product'], |
|||
|
|||
'data': [ |
|||
'views/product.xml', # uncomment this line only if you added website_sale as dependency. |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'eis_drag_drop/static/src/css/style.scss', |
|||
'eis_drag_drop/static/src/css/styles.css', |
|||
'eis_drag_drop/static/src/js/dnd_image_widget.js', |
|||
'eis_drag_drop/static/src/js/dnd_images_widget.js', |
|||
'eis_drag_drop/static/src/xml/dnd_widgets_templates.xml', |
|||
], |
|||
}, |
|||
'images': ['static/description/banner.gif'], |
|||
'live_test_url': 'https://www.youtube.com/watch?v=JRPchiEhS9U', |
|||
'price': 70.0, |
|||
'currency': 'EUR', |
|||
'auto_install': False, |
|||
'installable': True, |
|||
} |
|||
|
|||
|
@ -0,0 +1,949 @@ |
|||
.. container:: top-bar d-flex justify-content-between align-items-center |
|||
|
|||
.. container:: |
|||
|
|||
|Logo| |
|||
|
|||
.. container:: availability |
|||
|
|||
Community Enterprise Odoo Online |
|||
|
|||
.. container:: section hero-section |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: Images Drag And Drop Widgets |
|||
:name: images-drag-and-drop-widgets |
|||
|
|||
Effortlessly manage and update images in your Odoo records with |
|||
our intuitive drag and drop widgets. |
|||
|
|||
.. container:: vector-placeholder |
|||
|
|||
|Hero Vector| |
|||
|
|||
.. container:: benefit-box |
|||
|
|||
Perfect for E-commerce, Media Portals & More! |
|||
|
|||
.. container:: section section |
|||
:name: introduction |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
Introduction |
|||
:name: introduction |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
The **Images Drag And Drop Widgets** module is a state-of-the-art |
|||
solution designed to enhance your Odoo experience by integrating |
|||
modern, user-friendly drag and drop image widgets. |
|||
|
|||
.. rubric:: Who Can Benefit? |
|||
:name: who-can-benefit |
|||
|
|||
Whether you run an e-commerce site, a media portal, a real estate |
|||
listing platform, or any business that requires the handling of |
|||
multiple images per record, this module is built for you. Enjoy: |
|||
|
|||
- **E-commerce websites:** Easily manage product images with a |
|||
simple drag and drop interface. |
|||
- **Businesses with heavy media content:** Quickly update |
|||
portfolios, galleries, and listings. |
|||
- **Developers and Designers:** Seamlessly integrate modern |
|||
widgets across all modules—no extra dependency required! |
|||
|
|||
.. rubric:: Use Cases |
|||
:name: use-cases |
|||
|
|||
**Online Stores:** Showcase products with multiple images that can |
|||
be easily updated on-the-fly. |
|||
|
|||
**Real Estate Platforms:** Effortlessly manage property photos, |
|||
floor plans, and virtual tours. |
|||
|
|||
**Agencies & Portfolios:** Create visually appealing portfolios |
|||
where media speaks louder than words. |
|||
|
|||
.. rubric:: Benefits |
|||
:name: benefits |
|||
|
|||
With our module, say goodbye to tedious file uploads and hello to |
|||
a more engaging user experience. It’s simple, robust, and designed |
|||
to boost your productivity! |
|||
|
|||
.. container:: section section |
|||
:name: installation |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
Installation |
|||
:name: installation |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
Follow these detailed steps to install the **eis_drag_drop** |
|||
module: |
|||
|
|||
#. **Download the Module:** Obtain the module as a ZIP file from |
|||
the official repository or your vendor. |
|||
#. **Extract the ZIP:** Unzip the downloaded file to reveal the |
|||
module folder. |
|||
#. **Copy to Addons Folder:** Move or copy the extracted module |
|||
folder into your Odoo ``addons`` directory. |
|||
#. **Restart Odoo Service:** |
|||
|
|||
- On systemd-based systems: ``sudo systemctl restart odoo`` |
|||
- On SysVinit-based systems: ``sudo service odoo restart`` |
|||
- Or use the appropriate command for your OS. |
|||
|
|||
#. **Update App List:** In your Odoo database, navigate to the |
|||
Apps menu and click *Update Apps List*. |
|||
#. **Install the Module:** Search for *Drag and Drop Widgets* (or |
|||
``eis_drag_drop``) and click install. |
|||
|
|||
.. container:: tips |
|||
|
|||
**Important:** Always test the module in a staging environment |
|||
before deploying it to your production server. Caution is |
|||
key—ensure you have backups and proper rollback plans in place. |
|||
|
|||
.. container:: section section |
|||
:name: user-guides |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
User Guide |
|||
:name: user-guide |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. rubric:: Updating Existing Image Fields |
|||
:name: updating-existing-image-fields |
|||
|
|||
You can update any existing binary image field to use our drag and |
|||
drop widget without altering your custom modules. Simply update |
|||
your XML views by setting the widget attribute to: |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<field name="image_field" widget="d_and_d_image" options="{ |
|||
"image_size": "150x150", |
|||
"preview_image": "image_128", |
|||
"acceptedFileExtensions": "image/*", |
|||
"enableZoom": true, |
|||
"additionalStyles": "border:2px solid #007bff;" |
|||
}> |
|||
</field> |
|||
|
|||
**Options Explained:** |
|||
|
|||
- ``image_size``: Defines the display size of the widget. Example: |
|||
"150x150" sets the width to 150px and height to 150px. |
|||
- ``preview_image``: Specifies the field used for the image |
|||
preview. If not set, it defaults to the binary field name. |
|||
- ``acceptedFileExtensions``: Determines which image formats can |
|||
be uploaded. |
|||
- ``enableZoom``: If true, hovering over the image displays a zoom |
|||
popup. |
|||
- ``additionalStyles``: Custom CSS styles that are appended to the |
|||
widget's inline styles. |
|||
|
|||
.. rubric:: Detailed O2M Setup & Custom Integration |
|||
:name: detailed-o2m-setup-custom-integration |
|||
|
|||
For One2Many fields, our widget transforms the standard image |
|||
upload into a dynamic drag and drop area. **Note:** The related |
|||
model *must* have a Kanban view defined that includes at least the |
|||
``name`` and ``image`` (or ``image_1920``) fields. |
|||
|
|||
**Why a Kanban View?** The Kanban view enables the widget to |
|||
render image previews instead of just record IDs. Without it, you |
|||
may only see numerical identifiers. |
|||
|
|||
**Form View for Videos & Manual Entry:** If you wish to add videos |
|||
or manually add images in the traditional way, ensure that your |
|||
One2Many field is also accessible via a form view. |
|||
|
|||
.. rubric:: Example of One2Many Field Integration |
|||
:name: example-of-one2many-field-integration |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<page name="media" string="Extra Media"> |
|||
<field name="media_ids" widget="d_and_d_images" |
|||
options="{ |
|||
'childImageField': 'image_1920', |
|||
'extraData': { |
|||
'categ_id': 1, |
|||
'enable_zoom': true, |
|||
'previewImage': 'image_128', |
|||
'cssStyles': 'width:150px; height:200px; border-radius:15px;' |
|||
} |
|||
}"/> |
|||
</page> |
|||
|
|||
.. rubric:: Options Explained: |
|||
:name: options-explained |
|||
|
|||
- ``childImageField``: Specifies the field in the child model |
|||
where the binary image data is stored. This is mandatory for the |
|||
widget to function correctly. If not specified, image_1920 will |
|||
be used. |
|||
- ``extraData``: Allows you to pass additional data to the child |
|||
records. This can include: |
|||
|
|||
- ``categ_id``: An example field that sets a default category |
|||
ID. This feature allows you to add any additional field and |
|||
its value if your model has a required field and without it, |
|||
it cannot create new records, or a field that you need to fill |
|||
with static data. |
|||
- ``sale_ok``: This is another example field and you can pass |
|||
any bool value if your required field is bool type |
|||
- ``previewImage``: Specifies a smaller image field to use for |
|||
previews, which is important if you want to show a smaller |
|||
size image to save bandwidth. This value should be used if |
|||
your model is inherited from ``image.mixin`` and you want to |
|||
display a smaller preview image. |
|||
- ``cssStyles``: Custom CSS styles for the image display. |
|||
Example: |
|||
``"width:150px; height:200px; border-radius:15px; object-fit:cover;"``. |
|||
|
|||
- ``acceptedFileExtensions``: Determines which file types are |
|||
acceptable for upload. Default is ``"image/*"``. |
|||
|
|||
.. container:: section section |
|||
:name: user-guides |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
User Guide |
|||
:name: user-guide-1 |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. rubric:: Updating Existing Image Fields |
|||
:name: updating-existing-image-fields-1 |
|||
|
|||
You can update any existing binary image field to use our drag and |
|||
drop widget without altering your custom modules. Simply update |
|||
your XML views by setting the widget attribute to: |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<field name="image_field" widget="d_and_d_image" options="{ |
|||
"image_size": "150x150", |
|||
"preview_image": "image_128", |
|||
"acceptedFileExtensions": "image/*", |
|||
"enableZoom": true, |
|||
"additionalStyles": "border:2px solid #007bff;" |
|||
}> |
|||
</field> |
|||
|
|||
**Options Explained:** |
|||
|
|||
- ``image_size``: Defines the display size of the widget. Example: |
|||
"150x150" sets the width to 150px and height to 150px. |
|||
- ``preview_image``: Specifies the field used for the image |
|||
preview. If not set, it defaults to the binary field name. |
|||
- ``acceptedFileExtensions``: Determines which image formats can |
|||
be uploaded. |
|||
- ``enableZoom``: If true, hovering over the image displays a zoom |
|||
popup. |
|||
- ``additionalStyles``: Custom CSS styles that are appended to the |
|||
widget's inline styles. |
|||
|
|||
.. rubric:: Detailed O2M Setup & Custom Integration |
|||
:name: detailed-o2m-setup-custom-integration-1 |
|||
|
|||
For One2Many fields, our widget transforms the standard image |
|||
upload into a dynamic drag and drop area. **Note:** The related |
|||
model *must* have a Kanban view defined that includes at least the |
|||
``name`` and ``image`` (or ``image_1920``) fields. |
|||
|
|||
**Why a Kanban View?** The Kanban view enables the widget to |
|||
render image previews instead of just record IDs. Without it, you |
|||
may only see numerical identifiers. |
|||
|
|||
**Form View for Videos & Manual Entry:** If you wish to add videos |
|||
or manually add images in the traditional way, ensure that your |
|||
One2Many field is also accessible via a form view. |
|||
|
|||
.. rubric:: Example of One2Many Field Integration |
|||
:name: example-of-one2many-field-integration-1 |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<page name="media" string="Extra Media"> |
|||
<field name="media_ids" widget="d_and_d_images" |
|||
options="{ |
|||
'childImageField': 'image_1920', |
|||
'extraData': { |
|||
'categ_id': 1, |
|||
'enable_zoom': true, |
|||
'previewImage': 'image_128', |
|||
'cssStyles': 'width:150px; height:200px; border-radius:15px;' |
|||
} |
|||
}"/> |
|||
</page> |
|||
|
|||
.. rubric:: Options Explained: |
|||
:name: options-explained-1 |
|||
|
|||
- ``childImageField``: Specifies the field in the child model |
|||
where the binary image data is stored. This is mandatory for the |
|||
widget to function correctly. If not specified, ``image_1920`` |
|||
will be used. |
|||
- ``extraData``: Allows you to pass additional data to the child |
|||
records. This can include: |
|||
|
|||
- ``categ_id``: An example field that sets a default category |
|||
ID. This feature allows you to add any additional field and |
|||
its value if your model has a required field and without it, |
|||
it cannot create new records, or a field that you need to fill |
|||
with static data. |
|||
- ``sale_ok``: This is another example field and you can pass |
|||
any bool value if your required field is bool type. |
|||
- ``previewImage``: Specifies a smaller image field to use for |
|||
previews, which is important if you want to show a smaller |
|||
size image to save bandwidth. This value should be used if |
|||
your model is inherited from ``image.mixin`` and you want to |
|||
display a smaller preview image. |
|||
- ``cssStyles``: Custom CSS styles for the image display. |
|||
Example: |
|||
``"width:150px; height:200px; border-radius:15px; object-fit:cover;"``. |
|||
|
|||
- ``acceptedFileExtensions``: Determines which file types are |
|||
acceptable for upload. Default is ``"image/*"``. |
|||
|
|||
.. container:: alert alert-danger |
|||
|
|||
.. rubric:: Important Notice! |
|||
:name: important-notice |
|||
:class: alert-heading |
|||
|
|||
Do not create the models described in the following example. |
|||
Odoo already includes these models. This example is provided |
|||
for educational purposes to help you understand the |
|||
implementation. |
|||
|
|||
.. rubric:: Complete Example (Taken from Odoo builtin Code): |
|||
:name: complete-example-taken-from-odoo-builtin-code |
|||
|
|||
.. rubric:: Adding Multiple Image Support to ``product.template`` |
|||
:name: adding-multiple-image-support-to-product.template |
|||
|
|||
Let's say we have a model ``product.template`` and we need to add |
|||
multiple image support to it. We will create a model |
|||
``product.image`` by inheriting from the ``image.mixin`` class. |
|||
This way, we automatically get fields like ``image_1920`` and |
|||
other related fields for our model ``product.image``. |
|||
|
|||
Here are the steps to achieve this: |
|||
|
|||
#. **Create the ``product.image`` Model:** |
|||
|
|||
Inherit from ``image.mixin`` to automatically get image-related |
|||
fields. |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
class ProductImage(models.Model): |
|||
_name = 'product.image' |
|||
_inherit = 'image.mixin' |
|||
|
|||
product_tmpl_id = fields.Many2one('product.template', string='Product Template') |
|||
sequence = fields.Integer('Sequence') |
|||
video_url = fields.Char('Video URL') |
|||
|
|||
#. **Extend the ``product.template`` Model:** |
|||
|
|||
Add a One2Many field to link multiple images. |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
class ProductTemplate(models.Model): |
|||
_inherit = 'product.template' |
|||
|
|||
product_template_image_ids = fields.One2many('product.image', 'product_tmpl_id', string='Images') |
|||
|
|||
#. **Create a Kanban View for ``product.image``:** |
|||
|
|||
Add a Kanban view with the necessary fields. |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<record id="view_product_image_kanban" model="ir.ui.view"> |
|||
<field name="name">product.image.kanban</field> |
|||
<field name="model">product.image</field> |
|||
<field name="arch" type="xml"> |
|||
<kanban> |
|||
<field name="id"/> |
|||
<field name="name"/> |
|||
<field name="image_1920" widget="image"/> |
|||
<field name="sequence"/> |
|||
</kanban> |
|||
</field> |
|||
</record> |
|||
|
|||
#. **Create a Form View for ``product.image``:** |
|||
|
|||
Add a form view with fields for video URL and image. |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<record id="view_product_image_form" model="ir.ui.view"> |
|||
<field name="name">product.image.form</field> |
|||
<field name="model">product.image</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<field name="id"/> |
|||
<field name="name"/> |
|||
<field name="video_url"/> |
|||
<field name="image_1920" widget="image"/> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
#. **Update the ``product.template`` Form View:** |
|||
|
|||
Add the One2Many field with the drag-and-drop widget. |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
:: |
|||
|
|||
<record id="view_product_template_form" model="ir.ui.view"> |
|||
<field name="name">product.template.form</field> |
|||
<field name="model">product.template</field> |
|||
<field name="inherit_id" ref="product.product_template_only_form_view"/> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<sheet> |
|||
<group> |
|||
<field name="product_template_image_ids" widget="d_and_d_images" options="{ |
|||
'childImageField': 'image_1920', |
|||
'extraData': { |
|||
'cssStyles': 'width:150px;height:150px;' |
|||
} |
|||
}"/> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
.. container:: alert alert-warning |
|||
|
|||
**Important Considerations for Product Image Management (Odoo |
|||
17 and 18)** |
|||
This guide outlines the necessary steps for integrating |
|||
d_and_d_images widget for product template, specifically when |
|||
using the ``website_sale`` module, across Odoo 17 and 18. |
|||
Please carefully follow the instructions for your respective |
|||
Odoo version to avoid conflicts and ensure proper |
|||
functionality. |
|||
|
|||
**Odoo 17:** |
|||
|
|||
#. **Do not duplicate the ``product.image`` model or its |
|||
views.** This model is already provided by the |
|||
``website_sale`` module in Odoo. Creating a duplicate will |
|||
lead to conflicts. |
|||
#. **Inherit the ``product.template`` form view.**. You will |
|||
modify the form view ``product.product_template_form_view`` |
|||
to integrate d_and_d_images customizations seamlessly with |
|||
Odoo's core functionality. |
|||
#. **Customize the image field:** Replace the |
|||
``product_template_image_ids`` field in the inherited form |
|||
view with d_and_d_images widget as described above to |
|||
enhance the product image management functionality. |
|||
|
|||
**Odoo 18:** |
|||
|
|||
Odoo 18 has modified the ``product.image`` Kanban view by |
|||
removing certain fields. So, to make it compatible again with |
|||
our d_and_d_images widghet, here's how to adjust Odoo 18: |
|||
|
|||
#. **Inherit the ``product.image`` Kanban view.** |
|||
``website_sale.product_image_view_kanban``. Add the |
|||
necessary ``id`` and ``name`` fields back into the view to |
|||
maintain existing functionality. That's it. |
|||
#. **Enable modifications through the module's manifest:** |
|||
|
|||
- Declare dependency on ``website_sale`` in our |
|||
eis_drag_drop module's ``__manifest__.py`` file to ensure |
|||
proper loading order. You jsut need to uncomment the line |
|||
having website_sale with key depends and then comment the |
|||
existing depend key. |
|||
- Reference the XML file containing your Kanban view |
|||
modifications in the ``data`` key of the manifest to apply |
|||
these changes automatically when your module is installed |
|||
or updated. |
|||
|
|||
#. **Alternative approach:** Manually define the Kanban view |
|||
inheritance in a ``product.xml`` file if you prefer more |
|||
direct control over the modifications. |
|||
|
|||
Our module has pre-configured these enhancements for you. To |
|||
activate them, modify the ``__manifest__.py`` file to uncomment |
|||
the dependency on ``website_sale`` and the reference to |
|||
``/views/product.xml`` in the ``data`` key. This ensures all |
|||
functionalities are seamlessly integrated when you install or |
|||
update the module. |
|||
|
|||
.. container:: section section |
|||
:name: screenshots |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
Screenshots Explaination |
|||
:name: screenshots-explaination |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. container:: screenshot-full |
|||
|
|||
.. container:: screenshot-number |
|||
|
|||
01 |
|||
|
|||
.. rubric:: Drag and Drop Widget Overview |
|||
:name: drag-and-drop-widget-overview |
|||
|
|||
|Drag and Drop Widget Overview| |
|||
A clear view of the drag and drop widget areas for single |
|||
binary image and for one2many images., allowing users to |
|||
effortlessly update images. |
|||
|
|||
.. container:: screenshot-full |
|||
|
|||
.. container:: screenshot-number |
|||
|
|||
02 |
|||
|
|||
.. rubric:: One2Many Drag And Drop Explained |
|||
:name: one2many-drag-and-drop-explained |
|||
|
|||
|One2many Field Enhancement| |
|||
In this screenshot, you can see a simple explanation of the |
|||
view. Each record's thumbnail, have a small cross on top right |
|||
corner to remove a record, view also display video thumbnail, |
|||
and buttons. |
|||
|
|||
.. container:: screenshot-full |
|||
|
|||
.. container:: screenshot-number |
|||
|
|||
04 |
|||
|
|||
.. rubric:: Add Video Button |
|||
:name: add-video-button |
|||
|
|||
|image1| |
|||
This screenshot illustrates how the one2many field widget |
|||
``'Add Video'`` button is working. You can access odoo builtin |
|||
method being used in website_sale module of odoo to add extra |
|||
media including videos or images. |
|||
|
|||
.. container:: screenshot-full |
|||
|
|||
.. container:: screenshot-number |
|||
|
|||
05 |
|||
|
|||
.. rubric:: Manual File Uploding Button Display |
|||
:name: manual-file-uploding-button-display |
|||
|
|||
|image2| |
|||
This screenshot illustrates how the one2many field widget |
|||
manual upload button is working. If you want to upload images |
|||
manually from file uploader, you can click on this button and |
|||
it will open finder / file explorer for you. It will only allow |
|||
images to upload with jpg, jpeg, png, svg etc |
|||
|
|||
.. container:: screenshot-full |
|||
|
|||
.. container:: screenshot-number |
|||
|
|||
06 |
|||
|
|||
.. rubric:: Iamges Dragging over One2many Field Display |
|||
:name: iamges-dragging-over-one2many-field-display |
|||
|
|||
|image3| |
|||
This screenshot illustrates how the one2many field is being |
|||
used to drop 4 images into drag drop area. Notice the blue |
|||
border that is highlighted when we dragged the images over it. |
|||
As soon as we will drop the images, they will be uploaded |
|||
automatically |
|||
|
|||
.. container:: section section |
|||
:name: video |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
Video Explaination |
|||
:name: video-explaination |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. container:: screenshot-full |
|||
|
|||
.. container:: screenshot-number |
|||
|
|||
01 |
|||
|
|||
.. rubric:: Drag and Drop Widget Overview |
|||
:name: drag-and-drop-widget-overview-1 |
|||
|
|||
.. container:: video-container |
|||
|
|||
.. container:: iframe |
|||
|
|||
.. container:: |
|||
:name: player |
|||
|
|||
.. container:: player-unavailable |
|||
|
|||
.. rubric:: ایک خرابی پیش آ گئی۔ |
|||
:name: ایک-خرابی-پیش-آ-گئی |
|||
:class: message |
|||
|
|||
.. container:: submessage |
|||
|
|||
`اس ویڈیو کو www.youtube.com پر دیکھنے کی کوشش |
|||
کریں <https://www.youtube.com/watch?v=JRPchiEhS9U>`__ |
|||
یا اگر آپ کے براؤزر میں JavaScript غیر فعال ہے تو |
|||
اسے فعال کریں۔ |
|||
|
|||
Watch the video to see how it works. |
|||
|
|||
.. container:: section section |
|||
:name: version-info |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
Module Version Information & Changelog |
|||
:name: module-version-information-changelog |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. container:: version-info |
|||
|
|||
- **v1.0.0** - Initial release with basic drag and drop |
|||
functionality. |
|||
- **v1.1.0** - Added support for one2many fields and improved |
|||
preview handling. |
|||
- **v1.2.0** - Enhanced UI, added zoom popup, and refined |
|||
installation instructions. |
|||
- **v1.3.0** - Bug fixes and performance improvements for large |
|||
image uploads. |
|||
- **v1.3.1** - New feature added to save dirty form to preserver new changes. |
|||
- 1: - add new key 'showConfirm' in extraData, it is bool value. it will show a confirmation dialog to ask user to save changes in form or not. |
|||
- 2: - If no option given, the default is true. |
|||
|
|||
.. container:: section section |
|||
:name: services |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
90 Days Support |
|||
:name: days-support |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. container:: row text-center |
|||
|
|||
.. container:: col-md-4 mb-3 |
|||
|
|||
.. container:: service-tile |
|||
|
|||
.. rubric:: Website |
|||
:name: website |
|||
|
|||
`www.expertpk.com <https://www.expertpk.com>`__ |
|||
|
|||
.. container:: col-md-4 mb-3 |
|||
|
|||
.. container:: service-tile |
|||
|
|||
.. rubric:: Email |
|||
:name: email |
|||
|
|||
support@expertpk.com |
|||
|
|||
.. container:: col-md-4 mb-3 |
|||
|
|||
.. container:: service-tile |
|||
|
|||
.. rubric:: WhatsApp |
|||
:name: whatsapp |
|||
|
|||
+92 300 7888120 |
|||
|
|||
.. container:: section py-5 bg-light |
|||
:name: services |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: |
|||
Our Services |
|||
:name: our-services |
|||
:class: text-center mb-5 fw-bold |
|||
|
|||
.. container:: row g-4 |
|||
|
|||
.. container:: col-md-6 col-lg-3 |
|||
|
|||
.. container:: card h-100 text-center border-0 shadow-sm |
|||
|
|||
.. container:: card-body |
|||
|
|||
.. rubric:: Odoo Development |
|||
:name: odoo-development |
|||
:class: card-title |
|||
|
|||
Build custom Odoo modules and applications tailored to |
|||
your business needs. |
|||
|
|||
`Learn More <#contact>`__ |
|||
|
|||
.. container:: col-md-6 col-lg-3 |
|||
|
|||
.. container:: card h-100 text-center border-0 shadow-sm |
|||
|
|||
.. container:: card-body |
|||
|
|||
.. rubric:: Customization |
|||
:name: customization |
|||
:class: card-title |
|||
|
|||
Customize Odoo to fit your unique workflows and |
|||
business processes. |
|||
|
|||
`Learn More <#contact>`__ |
|||
|
|||
.. container:: col-md-6 col-lg-3 |
|||
|
|||
.. container:: card h-100 text-center border-0 shadow-sm |
|||
|
|||
.. container:: card-body |
|||
|
|||
.. rubric:: Upgradation |
|||
:name: upgradation |
|||
:class: card-title |
|||
|
|||
Upgrade your Odoo instance to the latest version with |
|||
zero downtime. |
|||
|
|||
`Learn More <#contact>`__ |
|||
|
|||
.. container:: col-md-6 col-lg-3 |
|||
|
|||
.. container:: card h-100 text-center border-0 shadow-sm |
|||
|
|||
.. container:: card-body |
|||
|
|||
.. rubric:: Consultancy |
|||
:name: consultancy |
|||
:class: card-title |
|||
|
|||
Get expert advice on Odoo implementation, |
|||
optimization, and best practices. |
|||
|
|||
`Learn More <#contact>`__ |
|||
|
|||
.. container:: section section |
|||
:name: other-modules |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: Explore Our Other Modules |
|||
:name: explore-our-other-modules |
|||
|
|||
.. container:: carousel slide module-slider |
|||
:name: modulesCarousel |
|||
|
|||
.. container:: carousel-inner |
|||
|
|||
.. container:: carousel-item active |
|||
|
|||
|Module 1| |
|||
|
|||
.. container:: carousel-caption d-none d-md-block |
|||
|
|||
.. rubric:: Module 1: Advanced Reporting |
|||
:name: module-1-advanced-reporting |
|||
|
|||
.. container:: carousel-item |
|||
|
|||
|Module 2| |
|||
|
|||
.. container:: carousel-caption d-none d-md-block |
|||
|
|||
.. rubric:: Module 2: CRM Enhancements |
|||
:name: module-2-crm-enhancements |
|||
|
|||
.. container:: carousel-item |
|||
|
|||
|Module 3| |
|||
|
|||
.. container:: carousel-caption d-none d-md-block |
|||
|
|||
.. rubric:: Module 3: Inventory Optimizer |
|||
:name: module-3-inventory-optimizer |
|||
|
|||
Previous |
|||
Next |
|||
|
|||
.. container:: section section |
|||
:name: best-practices |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: Best Practices & Troubleshooting |
|||
:name: best-practices-troubleshooting |
|||
|
|||
.. container:: tips |
|||
|
|||
.. rubric:: Best Practices |
|||
:name: best-practices |
|||
|
|||
- Always backup your database before applying new modules. |
|||
- Test the module in a staging environment prior to production |
|||
deployment. |
|||
- Ensure that the related one2many models have the required |
|||
Kanban view with mandatory fields. |
|||
- Regularly update your Odoo instance to keep up with security |
|||
and performance improvements. |
|||
|
|||
.. container:: troubleshooting |
|||
|
|||
.. rubric:: Troubleshooting Steps |
|||
:name: troubleshooting-steps |
|||
|
|||
- If image previews are not displaying, verify that your Kanban |
|||
view contains the ``name`` and ``image``/``image_1920`` |
|||
fields. |
|||
- Check the browser console for JavaScript errors related to |
|||
the widget. |
|||
- Ensure file permissions are correctly set in your addons |
|||
folder. |
|||
- Review the server logs for any errors during file upload or |
|||
service restart. |
|||
|
|||
.. container:: section section |
|||
:name: code-snippets |
|||
|
|||
.. container:: |
|||
|
|||
.. rubric:: Code Snippets |
|||
:name: code-snippets |
|||
|
|||
Below are examples of typical XML view definitions where the new |
|||
drag and drop fields are highlighted: |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
.. rubric:: Single Image Widget |
|||
:name: single-image-widget |
|||
|
|||
This snippet shows how to integrate a single image drag and |
|||
drop field into an Odoo form view: |
|||
|
|||
:: |
|||
|
|||
<form string="Product Form"> |
|||
<sheet> |
|||
<group> |
|||
<field name="name" /> |
|||
<field name="description" /> |
|||
<!-- Our new drag and drop image field is highlighted below --> |
|||
<field name="image_1920" widget="d_and_d_image" class="oe_avator" options="{ |
|||
"image_size": "200x200", |
|||
"preview_image": "image_128", |
|||
"acceptedFileExtensions": "image/*", |
|||
"enableZoom": true |
|||
}" /> |
|||
<field name="price" /> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
|
|||
**Explanation:** This code defines a form view for a product. |
|||
The ``product_image`` field uses the ``d_and_d_image`` widget, |
|||
which allows users to drag and drop images. The ``options`` |
|||
attribute specifies settings like image size, preview image, |
|||
accepted file extensions, and zoom functionality. |
|||
|
|||
.. container:: code-snippet |
|||
|
|||
.. rubric:: One2Many Image Widget |
|||
:name: one2many-image-widget |
|||
|
|||
This snippet demonstrates how to integrate a One2Many image |
|||
drag and drop field into an Odoo form view: |
|||
|
|||
:: |
|||
|
|||
<form string="Product Form"> |
|||
<sheet> |
|||
<group> |
|||
<field name="name" /> |
|||
<field name="description" /> |
|||
<!-- One2Many drag and drop image field --> |
|||
<field name="product_images" widget="d_and_d_images" options="{ |
|||
"childImageField": "image_1920", |
|||
"extraData": { |
|||
"cssStyles": "width:150px;height:150px;" |
|||
} |
|||
}" /> |
|||
<field name="price" /> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
|
|||
**Explanation:** This code defines a form view for a product |
|||
with a One2Many field ``product_images``. The |
|||
``d_and_d_images`` widget allows users to drag and drop |
|||
multiple images. The ``options`` attribute specifies the child |
|||
image field and additional CSS styles for the image display. |
|||
|
|||
© 2025 Expert IT Solutions. All rights reserved. |
|||
|
|||
.. |Logo| image:: ../static/description/img/logo.png |
|||
.. |Hero Vector| image:: ../static/description/img/hero.svg |
|||
.. |Drag and Drop Widget Overview| image:: ../static/description/img/image1.png |
|||
:class: screenshot-img |
|||
.. |One2many Field Enhancement| image:: ../static/description/img/image3.png |
|||
:class: screenshot-img |
|||
.. |image1| image:: ../static/description/img/image4.png |
|||
:class: screenshot-img |
|||
.. |image2| image:: ../static/description/img/image5.png |
|||
:class: screenshot-img |
|||
.. |image3| image:: ../static/description/img/image6.png |
|||
:class: screenshot-img |
|||
.. |Module 1| image:: ../static/description/module1.png |
|||
:class: d-block w-100 |
|||
.. |Module 2| image:: ../static/description/module2.png |
|||
:class: d-block w-100 |
|||
.. |Module 3| image:: ../static/description/module3.png |
|||
:class: d-block w-100 |
@ -0,0 +1 @@ |
|||
from . import models |
@ -0,0 +1,87 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################# |
|||
# Author : Expert IT Solutions (<www.expertpk.com>) |
|||
# Copyright(c): 2012-Present Expert IT Solutions |
|||
# All Rights Reserved. |
|||
# |
|||
# This program is copyright property of the author mentioned above. |
|||
# You can`t redistribute it and/or modify it. |
|||
# |
|||
################################################################################# |
|||
from odoo import models, fields, api |
|||
from lxml import etree |
|||
import logging |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class Base(models.AbstractModel): |
|||
_inherit = 'base' |
|||
|
|||
|
|||
@api.model |
|||
def get_view(self, view_id=None, view_type='form', **options): |
|||
res = super().get_view(view_id, view_type, **options) |
|||
# Parse the XML architecture from the result |
|||
doc = etree.XML(res['arch']) |
|||
if view_type == 'form': |
|||
for node in doc.xpath("//field[@widget='d_and_d_images']"): |
|||
_logger.info("Found node: %s", node.get('name')) |
|||
field_name = node.get('name') |
|||
|
|||
# Create a new field node with kanban mode and style display none |
|||
new_node = etree.Element('field', { |
|||
'name': field_name, |
|||
'mode': 'kanban', |
|||
'nolabel': '1', |
|||
'style': 'display:none; max-width: 1px; max-height: 1px;', |
|||
'context': "{'default_name': name}", |
|||
}) |
|||
parent = node.getparent() |
|||
index = parent.index(node) |
|||
parent.insert(index - 1, new_node) |
|||
|
|||
res['arch'] = etree.tostring(doc, pretty_print=True, encoding='unicode') |
|||
return res |
|||
return res |
|||
|
|||
|
|||
class IrAttachment(models.Model): |
|||
_inherit = "ir.attachment" |
|||
|
|||
@api.model |
|||
def action_save_drag_and_drop_images(self, resModel, resId, resField, childField, fileDatas, extraData=None): |
|||
parent = self.env[resModel].browse(int(resId)) |
|||
if not parent.exists(): |
|||
_logger.error(f"Parent record not found: Model={resModel}, ID={resId}") |
|||
return False |
|||
|
|||
records_data = [] |
|||
for fileData in fileDatas: |
|||
if 'filename' in fileData and 'base64' in fileData: |
|||
record_vals = { |
|||
'name': fileData['filename'], |
|||
childField: fileData['base64'], |
|||
} |
|||
if extraData and isinstance(extraData, dict): |
|||
filtered_data = {k: v for k, v in extraData.items() if k not in ['cssStyles', 'previewImage', 'showConfirm']} |
|||
record_vals.update(filtered_data) # Merge any extra required fields |
|||
records_data.append(record_vals) |
|||
else: |
|||
_logger.warning("Missing 'filename' or 'base64' in file data") |
|||
|
|||
if records_data: |
|||
try: |
|||
parent.write({ |
|||
resField: [(0, 0, vals) for vals in records_data] |
|||
}) |
|||
_logger.info(f"Images successfully added to {resModel} ID {resId} in field {resField}") |
|||
return True |
|||
except Exception as e: |
|||
_logger.error(f"Error updating parent record: {e}") |
|||
return False |
|||
else: |
|||
_logger.info("No valid image data provided") |
|||
return False |
|||
|
|||
|
After Width: | Height: | Size: 12 MiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 229 KiB |
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 6.2 KiB |
@ -0,0 +1,49 @@ |
|||
.o_dnd_images_widget { |
|||
width: 100%; |
|||
.o_dnd_drop_zone { |
|||
width: 100%; |
|||
min-height: 150px; |
|||
border: 2px dashed #ccc; |
|||
border-radius: 4px; |
|||
text-align: center; |
|||
position: relative; |
|||
transition: transform 0.2s ease-in-out; |
|||
.o_drop_area_text { |
|||
margin-top: 1em; |
|||
color: #999; |
|||
} |
|||
} |
|||
.o_dnd_drop_zone.o_dnd_dragging { |
|||
border: 3px dashed #007bff; |
|||
transform: scale(1.02); |
|||
background-color: #f8f9fa; |
|||
} |
|||
.o_dnd_uploading { |
|||
color: #666; |
|||
} |
|||
.o_dnd_kanban_container { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 1rem; |
|||
} |
|||
.o_dnd_kanban_card { |
|||
border: 1px solid #ddd; |
|||
border-radius: 4px; |
|||
width: 120px; |
|||
overflow: hidden; |
|||
.o_card_body { |
|||
position: relative; |
|||
.o_dnd_kanban_image { |
|||
width: 100%; |
|||
height: auto; |
|||
display: block; |
|||
} |
|||
.o_dnd_kanban_info { |
|||
padding: 4px; |
|||
background: #f2f2f2; |
|||
font-size: 0.8rem; |
|||
text-align: center; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,249 @@ |
|||
/* Enforce 100% width for the drag-and-drop zone */ |
|||
.o_dnd_images_widget { |
|||
width: 100%; |
|||
min-width: 100vh; |
|||
} |
|||
.o_dnd_drop_zone { |
|||
width: 100%; /* Force the container to expand to 100% width */ |
|||
min-height: 150px; /* Minimum height */ |
|||
border: 2px dashed #ccc; /* Dashed border */ |
|||
border-radius: 4px; /* Rounded corners */ |
|||
text-align: center; /* Center any child content */ |
|||
position: relative; /* Required for overlays/effects */ |
|||
transition: all 0.2s ease-in-out; /* Smooth interactions */ |
|||
background-color: #f8f8f8; /* Set a light background color so it's clear this area is the drop zone */ |
|||
box-sizing: border-box; /* Includes padding/border in layout */ |
|||
} |
|||
|
|||
/* Ensure the drop zone responds to drag events fully, not only the border */ |
|||
.o_dnd_drop_zone:hover, |
|||
.o_dnd_drop_zone.o_dnd_dragging { |
|||
background-color: #eef7ff; /* Highlight background during dragging */ |
|||
border-color: #007bff; /* Highlight border color */ |
|||
transform: scale(1.02); /* Slight zoom effect */ |
|||
} |
|||
|
|||
/* Add padding inside the drag-and-drop container */ |
|||
.o_dnd_drop_zone .o_dnd_kanban_container { |
|||
padding: 10px; /* Add spacing around the inner content */ |
|||
display: flex; /* Flex container for items inside drop zone */ |
|||
flex-wrap: wrap; /* Allow items to wrap to new rows */ |
|||
gap: 15px; /* Consistent spacing between tiles */ |
|||
justify-content: flex-start; /* Items start aligned to the left */ |
|||
align-items: flex-start; /* Top-aligned items */ |
|||
} |
|||
|
|||
/* Ensure the drop zone always maintains its design, even when empty */ |
|||
.o_dnd_drop_zone_empty { |
|||
display: flex; /* Center the "empty" placeholder */ |
|||
justify-content: center; /* Center horizontally */ |
|||
align-items: center; /* Center vertically */ |
|||
width: 100%; |
|||
height: 100%; /* Take full available drop zone space */ |
|||
color: #ccc; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
/* Flex-based item cards/tile styles */ |
|||
.o_dnd_kanban_card { |
|||
width: 120px; |
|||
height: 120px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background-color: #fff; /* White background for cards */ |
|||
border: 1px solid #ddd; /* Subtle card border */ |
|||
border-radius: 4px; /* Rounded corners */ |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Subtle shadow */ |
|||
position: relative; /* Required for close button positioning */ |
|||
} |
|||
|
|||
/* Close button inside cards */ |
|||
.o_dnd_kanban_card .close-button { |
|||
position: absolute; |
|||
top: -5px; |
|||
right: -5px; |
|||
background-color: red; |
|||
border: none; |
|||
color: white; |
|||
width: 20px; |
|||
height: 20px; |
|||
border-radius: 50%; |
|||
cursor: pointer; |
|||
font-size: 12px; |
|||
} |
|||
|
|||
/* Manual upload tile */ |
|||
.o_image_tile_upload { |
|||
background-color: #f8f9fa; /* Light gray background */ |
|||
border: 2px dashed #ccc; /* Dashed border for upload tile */ |
|||
width: 120px; /* Same size as other tiles */ |
|||
height: 120px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
transition: all 0.15s ease-in-out; /* Hover effect */ |
|||
} |
|||
|
|||
.o_image_tile_upload:hover { |
|||
border-color: #007bff; /* Highlight border on hover */ |
|||
background-color: #eef7ff; /* Subtle highlight background */ |
|||
} |
|||
|
|||
/* Ensure the upload indicator displays properly */ |
|||
.o_dnd_uploading { |
|||
color: #666; |
|||
text-align: center; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
/* single image styles below */ |
|||
.dnd_widget_container { |
|||
position: relative; |
|||
margin: 10px; |
|||
} |
|||
|
|||
.dnd_drop_zone { |
|||
position: relative; |
|||
border: 2px dashed #ccc; |
|||
border-radius: 8px; |
|||
padding: 10px; |
|||
text-align: center; |
|||
transition: border-color 0.3s; |
|||
} |
|||
|
|||
.dnd_active_drag { |
|||
border-color: #007bff; |
|||
} |
|||
|
|||
/* Controls hidden initially */ |
|||
.dnd_image_controls { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: space-between; |
|||
align-items: flex-start; |
|||
gap: 10px; |
|||
padding: 10px; |
|||
pointer-events: none; |
|||
opacity: 0; |
|||
transition: opacity 0.3s ease; |
|||
} |
|||
|
|||
.dnd_drop_zone:hover .dnd_image_controls { |
|||
pointer-events: auto; |
|||
opacity: 1; |
|||
} |
|||
|
|||
/* Edit Button (Top Left) */ |
|||
.dnd_edit_button { |
|||
position: absolute; |
|||
top: 10px; |
|||
left: 10px; |
|||
} |
|||
|
|||
/* Upload Button (Top Right) */ |
|||
.dnd_upload_button { |
|||
position: absolute; |
|||
top: 10px; |
|||
right: 10px; |
|||
} |
|||
|
|||
/* Remove Button (Bottom Left) */ |
|||
.dnd_remove_button { |
|||
position: absolute; |
|||
bottom: 10px; |
|||
left: 10px; |
|||
} |
|||
|
|||
.dnd_button { |
|||
background-color: white; |
|||
border: 1px solid #ccc; |
|||
border-radius: 50%; |
|||
padding: 5px; |
|||
cursor: pointer; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|||
z-index: 2; |
|||
} |
|||
|
|||
.dnd_button:hover { |
|||
background-color: #f9f9f9; |
|||
} |
|||
|
|||
/* Zoom popup remains unaffected */ |
|||
.dnd_zoom_popup { |
|||
position: absolute; |
|||
z-index: 9999; |
|||
transition: all 0.3s ease-in-out; |
|||
} |
|||
|
|||
.dnd_image_preview { |
|||
display: block; |
|||
object-fit: cover; |
|||
border-radius: 5px; |
|||
transition: transform 0.3s ease; |
|||
} |
|||
|
|||
.dnd_image_placeholder { |
|||
color: #aaa; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.dnd_uploading_spinner { |
|||
margin-top: 10px; |
|||
text-align: center; |
|||
color: #007bff; |
|||
} |
|||
|
|||
/* Style for the zoomed image */ |
|||
.dnd_zoom_popup { |
|||
display: flex; /* Use flexbox for centering the image */ |
|||
justify-content: center; |
|||
align-items: flex-start; /* Ensure the image aligns at the top inside the popup */ |
|||
position: absolute; /* Position the popup relative to the original element */ |
|||
/*top: 0; !* Ensure it aligns properly from the top *!*/ |
|||
/*margin-top: 40px;*/ |
|||
left: -110%; /* Place the popup to the left of the original block */ |
|||
z-index: 9999; /* Ensure popup appears above other elements */ |
|||
background-color: #fff; /* Add a background for better contrast */ |
|||
width: auto; /* Fixed width of 720p resolution */ |
|||
height: auto; /* Fixed height of 720p resolution */ |
|||
padding: 10px; |
|||
border-radius: 10px; |
|||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Add shadow for better visibility */ |
|||
border: 2px solid #007bff; /* Blue border for aesthetic */ |
|||
max-width: none; |
|||
max-height: none; |
|||
|
|||
/* Add scrollable behavior for content that overflows the fixed size */ |
|||
overflow: auto; /* Enable scrolling for overflow content */ |
|||
visibility: hidden; /* Hidden by default */ |
|||
opacity: 0; /* Initial opacity set to 0 */ |
|||
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; |
|||
} |
|||
|
|||
/* When zoom popup is visible */ |
|||
.dnd_zoom_popup.show { |
|||
visibility: visible; /* Make visible */ |
|||
opacity: 1; /* Add fade-in effect */ |
|||
} |
|||
|
|||
/* Zoomed image styling */ |
|||
.dnd_zoomed_image { |
|||
display: block; |
|||
/* Ensure the image expands or scrolls within the popup */ |
|||
max-width: 100%; /* Resize image width to fit popup */ |
|||
max-height: 100%; /* Resize image height to fit popup */ |
|||
width: auto; |
|||
height: auto; |
|||
object-fit: contain; /* Maintain the aspect ratio of the image */ |
|||
border-radius: 10px; /* Add border radius for aesthetics */ |
|||
} |
|||
/* single image styles above */ |
@ -0,0 +1,233 @@ |
|||
/** @odoo-module **/ |
|||
|
|||
import {registry} from "@web/core/registry"; |
|||
import {_t} from "@web/core/l10n/translation"; |
|||
import {FileUploader} from "@web/views/fields/file_handler"; |
|||
import {standardFieldProps} from "@web/views/fields/standard_field_props"; |
|||
import {url} from "@web/core/utils/urls"; |
|||
import {isBinarySize} from "@web/core/utils/binary"; |
|||
import {useService} from "@web/core/utils/hooks"; // optional if you want notifications
|
|||
import {Component, useState, useRef} from "@odoo/owl"; |
|||
import {onWillUpdateProps} from "@odoo/owl"; |
|||
|
|||
/** |
|||
* Enhanced single-image drag-and-drop widget with custom image size and preview features. |
|||
*/ |
|||
|
|||
const placeholder = "/web/static/img/placeholder.png"; |
|||
export class DndImageWidget extends Component { |
|||
static template = "eis_drag_drop.DndImageWidgetTemplate"; |
|||
static components = {FileUploader}; |
|||
static props = { |
|||
...standardFieldProps, |
|||
acceptedFileExtensions: {type: String, optional: true}, |
|||
image_size: {type: String, optional: true}, // Example: "100x150"
|
|||
preview_image: {type: String, optional: true}, // Example: "image_128"
|
|||
enableZoom: {type: Boolean, optional: true}, // Optional zoom functionality
|
|||
additionalStyles: {type: String, optional: true}, // Append custom styles
|
|||
}; |
|||
static defaultProps = { |
|||
acceptedFileExtensions: "image/*", |
|||
image_size: "100x150", |
|||
}; |
|||
|
|||
setup() { |
|||
this.notification = useService("notification"); |
|||
this.isUploading = useState({value: false}); |
|||
this.isDraggingOver = useState({value: false}); |
|||
this.dropZoneRef = useRef("dropZoneRef"); |
|||
this.zoomTimer = null; // Timer for delayed zoom
|
|||
this.showZoomPopup = useState({ value: false }); // Popup state
|
|||
|
|||
// Extract width/height from image_size
|
|||
const [width, height] = (this.props.image_size || "").split("x").map((value) => parseInt(value)); |
|||
this.imageWidth = isNaN(width) ? null : width; |
|||
this.imageHeight = isNaN(height) ? null : height; |
|||
|
|||
this.cacheKey = this.props.record.data.write_date; |
|||
onWillUpdateProps((nextProps) => { |
|||
const {record} = this.props; |
|||
const {record: nextRecord} = nextProps; |
|||
if (record.resId !== nextRecord.resId || nextRecord.mode === "readonly") { |
|||
this.cacheKey = nextRecord.data.write_date; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Event Methods
|
|||
//--------------------------------------------------------------------------
|
|||
// Handles hover on image (shows zoom popup after 1 second)
|
|||
onMouseEnter() { |
|||
if (this.props.enableZoom) { |
|||
this.zoomTimer = setTimeout(() => { |
|||
this.showZoomPopup.value = true; // Display popup
|
|||
}, 1000); // Trigger after 1-second hover
|
|||
} |
|||
} |
|||
|
|||
// Handles mouse leave (hides zoom popup)
|
|||
onMouseLeave() { |
|||
if (this.zoomTimer) { |
|||
clearTimeout(this.zoomTimer); // Cancel timeout if mouse leaves
|
|||
this.zoomTimer = null; |
|||
} |
|||
this.showZoomPopup.value = false; // Hide popup
|
|||
} |
|||
|
|||
// Prevents the popup from hiding when interacting within it
|
|||
onMouseEnterPopup() { |
|||
this.showZoomPopup.value = true; // Ensure visibility on hover within the popup
|
|||
} |
|||
|
|||
onMouseLeavePopup() { |
|||
this.showZoomPopup.value = false; // Hide popup when mouse leaves
|
|||
} |
|||
|
|||
onDownloadOriginalImage() { |
|||
const anchor = document.createElement("a"); |
|||
anchor.href = this.originalImageUrl; |
|||
anchor.download = "original_image"; // Default filename
|
|||
anchor.click(); |
|||
console.log("[DndImageWidget] Download initiated:", this.originalImageUrl); |
|||
} |
|||
|
|||
// Returns the URL for the original image (full resolution)
|
|||
get originalImageUrl() { |
|||
return url("/web/image", { |
|||
model: this.props.record.resModel, |
|||
id: this.props.record.resId, |
|||
field: this.props.name, // Always load original image
|
|||
unique: this.cacheKey || "", |
|||
}); |
|||
} |
|||
|
|||
get imageUrl() { |
|||
const bin = this.props.record.data[this.props.name]; |
|||
console.log('bin: ', bin); |
|||
if (!bin) return placeholder; |
|||
if (isBinarySize(bin)) { |
|||
let recUrl = url("/web/image", { |
|||
model: this.props.record.resModel, |
|||
id: this.props.record.resId, |
|||
field: this.props.preview_image || this.props.name, |
|||
unique: this.cacheKey || "", |
|||
}); |
|||
console.log('recUrl: ', recUrl); |
|||
return recUrl; |
|||
} |
|||
return `data:image/png;base64,${bin}`; |
|||
} |
|||
|
|||
get imageStyle() { |
|||
let style = this.isDraggingOver.value ? "border: 2px dashed #007bff;" : ""; // Dashed border on drag
|
|||
if (this.imageWidth) { |
|||
style += `width: ${this.imageWidth}px;`; |
|||
} |
|||
if (this.imageHeight) { |
|||
style += `height: ${this.imageHeight}px;`; |
|||
} else { |
|||
style += "max-width: 100%; max-height: 100%;"; |
|||
} |
|||
if (this.props.additionalStyles) { |
|||
style += this.props.additionalStyles; // Append user-defined styles
|
|||
} |
|||
style += 'object-fit: cover;'; |
|||
return style; |
|||
} |
|||
// Refresh the image URL when a new image is uploaded or replaced
|
|||
refreshImage() { |
|||
this.cacheKey = Date.now(); // Generate a unique cache key to force image reloading
|
|||
this.render(); // Trigger re-render to update the image
|
|||
} |
|||
|
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Event Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
onDrop(ev) { |
|||
ev.preventDefault(); |
|||
this.isDraggingOver.value = false; |
|||
const file = ev.dataTransfer.files[0]; |
|||
if (!file) return; |
|||
this._readFileAsDataUrl(file); |
|||
} |
|||
|
|||
onDragOver(ev) { |
|||
ev.preventDefault(); |
|||
} |
|||
|
|||
onDragEnter(ev) { |
|||
ev.preventDefault(); |
|||
this.isDraggingOver.value = true; |
|||
} |
|||
|
|||
onDragLeave(ev) { |
|||
ev.preventDefault(); |
|||
this.isDraggingOver.value = false; |
|||
} |
|||
|
|||
async _readFileAsDataUrl(file) { |
|||
this.isUploading.value = true; |
|||
const reader = new FileReader(); |
|||
reader.onload = (e) => { |
|||
const dataUrl = e.target.result; |
|||
this._updateRecordWithBase64(dataUrl); |
|||
this.refreshImage(); // Call refreshImage to update the displayed image
|
|||
}; |
|||
reader.readAsDataURL(file); |
|||
} |
|||
|
|||
_updateRecordWithBase64(dataUrl) { |
|||
const base64Part = dataUrl.split(",")[1] || ""; |
|||
this.props.record.update({[this.props.name]: base64Part}); |
|||
this.isUploading.value = false; |
|||
} |
|||
|
|||
onFileRemove() { |
|||
this.props.record.update({ [this.props.name]: false }); |
|||
this.refreshImage(); // Refresh the image if it is removed
|
|||
} |
|||
|
|||
onLoadFailed() { |
|||
this.notification?.add(_t("Could not display the selected image"), { |
|||
type: "warning", |
|||
}); |
|||
} |
|||
|
|||
onFileUploaded(info) { |
|||
if (info.data) { |
|||
const mimeSuffix = info.type.split("/")[1] || "png"; |
|||
const dataUrl = `data:image/${mimeSuffix};base64,${info.data}`; |
|||
this._updateRecordWithBase64(dataUrl); |
|||
|
|||
// Refresh the zoomed image after file upload
|
|||
this.refreshImage(); |
|||
} |
|||
} |
|||
|
|||
onClickImage() { |
|||
// Open image in a new tab
|
|||
window.open(this.originalImageUrl, "_blank"); |
|||
} |
|||
} |
|||
|
|||
// Debugging the extractProps logic
|
|||
const dndImageField = { |
|||
component: DndImageWidget, |
|||
displayName: _t("D&D Single Image"), |
|||
supportedTypes: ["binary"], |
|||
extractProps: ({attrs, options}) => { |
|||
console.log("[DndImageWidget] extractProps invoked - attrs:", attrs, "options:", options); |
|||
|
|||
return { |
|||
image_size: options.image_size || "100x120", // Fallback to "100x150"
|
|||
preview_image: options.preview_image || attrs.name, // Fallback to field name
|
|||
acceptedFileExtensions: options.acceptedFileExtensions || "image/*", // Default to all image types
|
|||
enableZoom: options.enableZoom === "true" || false, // Ensure boolean value
|
|||
}; |
|||
}, |
|||
}; |
|||
|
|||
// Register the widget
|
|||
registry.category("fields").add("d_and_d_image", dndImageField); |
@ -0,0 +1,254 @@ |
|||
/** @odoo-module **/ |
|||
import { registry } from "@web/core/registry"; |
|||
import { _t } from "@web/core/l10n/translation"; |
|||
import { standardFieldProps } from "@web/views/fields/standard_field_props"; |
|||
import { FileUploader } from "@web/views/fields/file_handler"; |
|||
import { Component, useState, useRef } from "@odoo/owl"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; |
|||
|
|||
export class DndImagesWidget extends Component { |
|||
static template = "eis_drag_drop.DndImagesWidgetTemplate"; |
|||
static components = { FileUploader }; |
|||
static props = { |
|||
...standardFieldProps, |
|||
childImageField: { type: String, optional: true }, |
|||
extraData: { type: Object, optional: true }, |
|||
}; |
|||
|
|||
setup() { |
|||
super.setup(); |
|||
this.orm = useService("orm"); |
|||
this.action = useService("action"); |
|||
this.notification = useService("notification"); |
|||
this.isUploading = useState({ value: false }); |
|||
this.isDragging = useState({ value: false }); |
|||
this.dropZoneRef = useRef("dropZoneRef"); |
|||
// If one2many not loaded, load it explicitly
|
|||
if (!this.props.record.data[this.props.name]) { |
|||
this.props.record.ensureOne2manyRecords(this.props.name); |
|||
} |
|||
} |
|||
|
|||
onDragOver(ev) { |
|||
ev.preventDefault(); |
|||
} |
|||
onDragEnter(ev) { |
|||
ev.preventDefault(); |
|||
this.isDragging.value = true; |
|||
} |
|||
onDragLeave(ev) { |
|||
ev.preventDefault(); |
|||
this.isDragging.value = false; |
|||
} |
|||
async onDrop(ev) { |
|||
ev.preventDefault(); |
|||
this.isDragging.value = false; |
|||
const files = ev.dataTransfer.files || []; |
|||
if (!files.length) { |
|||
return; |
|||
} |
|||
await this._handleMultipleFiles(files); |
|||
} |
|||
|
|||
async _handleMultipleFiles(files) { |
|||
this.isUploading.value = true; |
|||
try { |
|||
const extraData = this.props.extraData || {}; |
|||
// If parent record is dirty, confirm or just save
|
|||
if (this.props.record.isDirty) { |
|||
if (extraData.showConfirm) { |
|||
const confirmed = await new Promise(resolve => { |
|||
this.env.services.dialog.add(ConfirmationDialog, { |
|||
body: _t("The record has unsaved changes. Do you want to save them?"), |
|||
confirm: () => resolve(true), |
|||
cancel: () => resolve(false), |
|||
}); |
|||
}); |
|||
if (!confirmed) { |
|||
this.isUploading.value = false; |
|||
return; |
|||
} |
|||
} |
|||
await this.props.record.save({ reload: true }); |
|||
} |
|||
|
|||
// Build fileDatas array
|
|||
const fileDatas = []; |
|||
for (let i = 0; i < files.length; i++) { |
|||
const file = files[i]; |
|||
if (!file.type.startsWith("image/")) { |
|||
continue; |
|||
} |
|||
const dataUrl = await this._readFile(file); |
|||
const base64Part = dataUrl.split(",")[1] || ""; |
|||
fileDatas.push({ base64: base64Part, filename: file.name }); |
|||
} |
|||
|
|||
if (fileDatas.length) { |
|||
const resModel = this.props.record.resModel; |
|||
const resId = this.props.record.resId; |
|||
const resField = this.props.name; |
|||
const childField = this.props.childImageField || "image_1920"; |
|||
|
|||
await this.orm.call("ir.attachment", "action_save_drag_and_drop_images", [ |
|||
resModel, |
|||
resId, |
|||
resField, |
|||
childField, |
|||
fileDatas, |
|||
extraData, |
|||
]); |
|||
await this.props.record.load(); |
|||
await this.props.record.model.notify(); |
|||
} else { |
|||
this.notification.add(_t("No valid image files found."), { type: "warning" }); |
|||
} |
|||
} catch (error) { |
|||
console.error("Error uploading images:", error); |
|||
this.notification.add( |
|||
_t("Error uploading images. Check logs or required fields."), |
|||
{ type: "danger" } |
|||
); |
|||
} finally { |
|||
this.isUploading.value = false; |
|||
} |
|||
} |
|||
|
|||
_readFile(file) { |
|||
return new Promise((resolve, reject) => { |
|||
const reader = new FileReader(); |
|||
reader.onload = e => resolve(e.target.result); |
|||
reader.onerror = reject; |
|||
reader.readAsDataURL(file); |
|||
}); |
|||
} |
|||
|
|||
// When FileUploader (manual upload) fires:
|
|||
onFileUploaded = async (ev) => { |
|||
// If parent record is dirty, confirm or just save
|
|||
const extraData = this.props.extraData || {}; |
|||
if (this.props.record.isDirty) { |
|||
if (extraData.showConfirm) { |
|||
const confirmed = await new Promise(resolve => { |
|||
this.env.services.dialog.add(ConfirmationDialog, { |
|||
body: _t("The record has unsaved changes. Do you want to save them?"), |
|||
confirm: () => resolve(true), |
|||
cancel: () => resolve(false), |
|||
}); |
|||
}); |
|||
if (!confirmed) { |
|||
return; |
|||
} |
|||
} |
|||
await this.props.record.save({ reload: true }); |
|||
} |
|||
|
|||
if (!ev.data) { |
|||
this.notification.add(_t("No files uploaded or invalid file."), { type: "warning" }); |
|||
return; |
|||
} |
|||
|
|||
this.isUploading.value = true; |
|||
try { |
|||
const fileDatas = []; |
|||
// Normalize ev.data & ev.name to arrays
|
|||
const dataList = Array.isArray(ev.data) ? ev.data : [ev.data]; |
|||
const nameList = Array.isArray(ev.name) ? ev.name : [ev.name]; |
|||
for (let i = 0; i < dataList.length; i++) { |
|||
const base64String = dataList[i]; |
|||
const filename = nameList[i] || `file_${i}`; |
|||
fileDatas.push({ base64: base64String, filename: filename }); |
|||
} |
|||
|
|||
const resModel = this.props.record.resModel; |
|||
const resId = this.props.record.resId; |
|||
const resField = this.props.name; |
|||
const childField = this.props.childImageField || "image_1920"; |
|||
|
|||
await this.orm.call("ir.attachment", "action_save_drag_and_drop_images", [ |
|||
resModel, |
|||
resId, |
|||
resField, |
|||
childField, |
|||
fileDatas, |
|||
extraData, |
|||
]); |
|||
|
|||
await this.props.record.load(); |
|||
await this.props.record.model.notify(); |
|||
|
|||
} catch (error) { |
|||
console.error("Error during file upload:", error); |
|||
this.notification.add( |
|||
_t("Error uploading images. Check logs or required fields."), |
|||
{ type: "danger" } |
|||
); |
|||
} finally { |
|||
this.isUploading.value = false; |
|||
} |
|||
}; |
|||
|
|||
// Remove a single tile (child record)
|
|||
removeRecord = async (recordId) => { |
|||
try { |
|||
const commands = [[3, recordId]]; |
|||
await this.orm.call(this.props.record.resModel, "write", [ |
|||
this.props.record.resId, |
|||
{ [this.props.name]: commands }, |
|||
]); |
|||
await this.props.record.load(); |
|||
this.notification.add("Image removed successfully.", { type: "success" }); |
|||
} catch (error) { |
|||
console.error("Error removing image:", error); |
|||
this.notification.add("Failed to remove image.", { type: "danger" }); |
|||
} |
|||
}; |
|||
|
|||
// Open a blank “new child” form in a modal
|
|||
async openChildFormView() { |
|||
const childModel = this.props.record.data[this.props.name].resModel; |
|||
const inverseField = this.props.record.fields[this.props.name].relation_field; |
|||
const ctx = { |
|||
[`default_${inverseField}`]: this.props.record.resId, |
|||
default_parent_id: this.props.record.resId, |
|||
}; |
|||
try { |
|||
const action = { |
|||
type: "ir.actions.act_window", |
|||
res_model: childModel, |
|||
views: [[false, "form"]], |
|||
target: "new", |
|||
context: ctx, |
|||
}; |
|||
await this.action.doAction(action, { |
|||
onClose: async () => { |
|||
await this.props.record.load(); |
|||
this.props.record.model.notify(); |
|||
}, |
|||
}); |
|||
} catch (error) { |
|||
console.error("Error opening child form view:", error); |
|||
this.notification.add(_t("Error opening child form view."), { type: "danger" }); |
|||
} |
|||
} |
|||
|
|||
computeImageUrl(line) { |
|||
const model = this.props.record.data[this.props.name].resModel; |
|||
const field = this.props.extraData.previewImage || this.props.childImageField; |
|||
return `/web/image?model=${model}&id=${line.data.id}&field=${field}&unique=${line.data.write_date}`; |
|||
} |
|||
} |
|||
|
|||
const dndImagesField = { |
|||
component: DndImagesWidget, |
|||
displayName: _t("Enhanced DnD Multi Images"), |
|||
supportedTypes: ["one2many"], |
|||
extractProps: ({ attrs, options }) => { |
|||
return { |
|||
childImageField: options.childImageField || "image_1920", |
|||
extraData: options.extraData || {}, |
|||
}; |
|||
}, |
|||
}; |
|||
registry.category("fields").add("d_and_d_images", dndImagesField); |
@ -0,0 +1,192 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<templates xml:space="preserve"> |
|||
<!-- dnd_widgets_templates.xml --> |
|||
<t t-name="eis_drag_drop.DndImageWidgetTemplate"> |
|||
<div class="dnd_widget_container"> |
|||
<!-- Drop Zone --> |
|||
<div class="dnd_drop_zone" |
|||
t-ref="dropZoneRef" |
|||
t-att-class="{ |
|||
'dnd_active_drag': isDraggingOver.value, |
|||
'dnd_no_image': !imageUrl |
|||
}" |
|||
t-on-drop="onDrop" |
|||
t-on-dragover="onDragOver" |
|||
t-on-dragenter="onDragEnter" |
|||
t-on-dragleave="onDragLeave" |
|||
> |
|||
<!-- Controls (edit, remove, download) --> |
|||
<t t-if="imageUrl && !props.readonly"> |
|||
<div class="dnd_image_controls"> |
|||
<!-- Edit Button (Top Left) --> |
|||
<FileUploader onUploaded.bind="onFileUploaded" |
|||
acceptedFileExtensions="props.acceptedFileExtensions"> |
|||
<t t-set-slot="toggler"> |
|||
<button class="dnd_button dnd_edit_button" type="button"> |
|||
<i class="fa fa-pencil"/> |
|||
</button> |
|||
</t> |
|||
</FileUploader> |
|||
|
|||
<!-- Upload Button (Top Right) --> |
|||
<button t-if="props.record.data[props.name]" |
|||
class="dnd_button dnd_upload_button" |
|||
type="button" |
|||
t-on-click="onFileUploaded"> |
|||
<i class="fa fa-upload"/> |
|||
</button> |
|||
|
|||
<!-- Remove Button (Bottom Left) --> |
|||
<button t-if="props.record.data[props.name]" |
|||
class="dnd_button dnd_remove_button" |
|||
type="button" |
|||
t-on-click="onFileRemove"> |
|||
<i class="fa fa-trash-o"/> |
|||
</button> |
|||
</div> |
|||
</t> |
|||
|
|||
<!-- Image Preview --> |
|||
<img t-if="imageUrl" |
|||
loading="lazy" |
|||
t-att-src="imageUrl" |
|||
class="dnd_image_preview" |
|||
alt="Drag & Drop" |
|||
t-att-style="imageStyle" |
|||
t-on-mouseenter="onMouseEnter" |
|||
t-on-mouseleave="onMouseLeave" |
|||
t-on-click="onClickImage" |
|||
t-on-error="onLoadFailed" |
|||
/> |
|||
|
|||
<!-- Popup for zooming --> |
|||
<div class="dnd_zoom_popup" |
|||
t-att-class="{ show: showZoomPopup.value }" |
|||
t-on-mouseenter="onMouseEnterPopup" |
|||
t-on-mouseleave="onMouseLeavePopup"> |
|||
<a t-att-href="originalImageUrl" target="_blank" rel="noopener noreferrer"> |
|||
<img t-att-src="originalImageUrl" class="dnd_zoomed_image"/> |
|||
</a> |
|||
</div> |
|||
|
|||
<!-- Placeholder if no image is available --> |
|||
<t t-if="!imageUrl"> |
|||
<div class="dnd_image_placeholder"> |
|||
<i class="fa fa-image"></i> |
|||
<p>Drag & Drop Image Here</p> |
|||
</div> |
|||
</t> |
|||
</div> |
|||
|
|||
<!-- Uploading Spinner --> |
|||
<t t-if="isUploading.value"> |
|||
<div class="dnd_uploading_spinner"> |
|||
<i class="fa fa-spinner fa-spin"/> Uploading... |
|||
</div> |
|||
</t> |
|||
</div> |
|||
</t> |
|||
|
|||
|
|||
<!-- template for multiple images --> |
|||
<t t-name="eis_drag_drop.DndImagesWidgetTemplate"> |
|||
<div class="o_dnd_images_widget"> |
|||
<!-- Drag-and-drop container --> |
|||
<div t-att-class="{ |
|||
'o_dnd_drop_zone': true, |
|||
'o_dnd_dragging': isDragging.value |
|||
}" |
|||
t-ref="dropZoneRef" |
|||
t-on-drop.prevent="onDrop" |
|||
t-on-dragover="onDragOver" |
|||
t-on-dragenter="onDragEnter" |
|||
t-on-dragleave="onDragLeave" |
|||
style="width: 100%;"> |
|||
|
|||
<!-- Handle empty state with placeholder text --> |
|||
<t t-if="(props.record.data[props.name].records or []).length === 0"> |
|||
<!-- Manual Upload Tile --> |
|||
<div class="o_dnd_kanban_container"> |
|||
<div class="o_image_tile_upload"> |
|||
<FileUploader onUploaded.bind="onFileUploaded" multiUpload="true" |
|||
acceptedFileExtensions="'.bmp, .jpg, .jpeg, .png, .svg, .webp'"> |
|||
<div class="o_image_tile_inner"> |
|||
<t t-set-slot="toggler"> |
|||
<input type="text" class="o_input" t-att-value="fileName" readonly="readonly"/> |
|||
<button |
|||
class="btn btn-link btn-sm lh-1 fa fa-pencil o_select_file_button" |
|||
data-tooltip="Add" |
|||
aria-label="Add" |
|||
> Drag or Upload |
|||
</button> |
|||
</t> |
|||
</div> |
|||
</FileUploader> |
|||
</div> |
|||
<button type="button" |
|||
class="btn btn-secondary my-2" |
|||
t-on-click="openChildFormView"> |
|||
Add Video |
|||
</button> |
|||
</div> |
|||
|
|||
</t> |
|||
|
|||
<!-- Existing Image Tiles --> |
|||
<div t-else="" class="o_dnd_kanban_container"> |
|||
<t t-set="childLines" t-value="props.record.data[props.name].records or []"/> |
|||
<t t-set="childModel" t-value="props.record.data[props.name].resModel"/> |
|||
|
|||
<!-- Display existing images as tiles --> |
|||
<t t-foreach="childLines" t-as="line" t-key="line.data.id"> |
|||
<div class="o_dnd_kanban_card"> |
|||
<img t-att-src="computeImageUrl(line)" t-att-alt="line.data.name" |
|||
t-attf-style="object-fit:cover; #{props.extraData.cssStyles || 'width:100px; height:100px' }" |
|||
class="o_dnd_kanban_image "/> |
|||
|
|||
<button type="button" class="close-button" |
|||
t-on-click.prevent="() => removeRecord(line.data.id)"> |
|||
X |
|||
</button> |
|||
</div> |
|||
</t> |
|||
|
|||
<!-- Manual Upload Tile --> |
|||
<div class="o_image_tile_upload"> |
|||
<FileUploader onUploaded.bind="onFileUploaded" multiUpload="true" |
|||
acceptedFileExtensions="'.bmp, .jpg, .jpeg, .png, .svg, .webp'"> |
|||
<div class="o_image_tile_inner"> |
|||
<t t-set-slot="toggler"> |
|||
<input type="text" class="o_input" t-att-value="fileName" readonly="readonly"/> |
|||
<button |
|||
class="btn btn-link btn-sm lh-1 fa fa-pencil o_select_file_button" |
|||
data-tooltip="Add" |
|||
aria-label="Add" |
|||
> Drag or Upload |
|||
</button> |
|||
</t> |
|||
</div> |
|||
</FileUploader> |
|||
</div> |
|||
<!-- Button to open form view in popup --> |
|||
<button type="button" |
|||
class="btn btn-secondary my-2" |
|||
t-on-click="openChildFormView"> |
|||
Add Video |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<!-- Upload in Progress Indicator --> |
|||
<t t-if="isUploading.value"> |
|||
<div class="o_dnd_uploading"> |
|||
<i class="fa fa-spinner fa-spin"></i> Uploading... |
|||
</div> |
|||
</t> |
|||
|
|||
</div> |
|||
</t> |
|||
|
|||
|
|||
</templates> |
@ -0,0 +1,46 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<data> |
|||
|
|||
|
|||
<!-- Inherit Form View to Modify it --> |
|||
<record id="inherit_product_template_common" model="ir.ui.view"> |
|||
<field name="name">inherit.product.template.common.faheem</field> |
|||
<field name="model">product.template</field> |
|||
<field name="inherit_id" ref="product.product_template_form_view"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//field[@name='image_1920']" position="replace"> |
|||
<field name="image_1920" widget="d_and_d_image" class="oe_avatar" |
|||
options="{'image_size': '100x100', 'preview_image': 'image_128'}"/> |
|||
<!-- Add your fields or attributes here --> |
|||
</xpath> |
|||
<xpath expr="//field[@name='product_template_image_ids']" position="replace"> |
|||
<field name="product_template_image_ids" nolabel="1" widget="d_and_d_images" options="{ |
|||
'childImageField': 'image_1920', |
|||
'extraData': { |
|||
'previewImage': 'image_128', |
|||
}}"/> |
|||
|
|||
|
|||
<!-- Add your fields or attributes here --> |
|||
</xpath> |
|||
|
|||
</field> |
|||
</record> |
|||
|
|||
<!-- Inherit Kanban View of odoo 18 to add missing id and name fields in it --> |
|||
<!-- <record id="product_image_kanban_view_inherit" model="ir.ui.view"> |
|||
<field name="name">product.view.image.kanban.inherit</field> |
|||
<field name="model">product.image</field> |
|||
<field name="inherit_id" ref="website_sale.product_image_view_kanban"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="video_url" position="after"> |
|||
<field name="id" invisible="1"/> |
|||
<field name="name" invisible="1"/> |
|||
</field> |
|||
</field> |
|||
</record> --> |
|||
|
|||
|
|||
</data> |
|||
</odoo> |