[IMP] mrp: migrate mrp_documents_kanban to wowl2
This views has multiple custom features such as file upload, attachment viewer and download. A lot of files have been rendered obsolete and have been deleted, notably `file_upload_mixin` which has been replaced by the file_upload service and hooks. closes odoo/odoo#99769 Related: odoo/enterprise#31145 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
@@ -1,415 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import core from 'web.core';
|
||||
import Widget from 'web.Widget';
|
||||
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var SCROLL_ZOOM_STEP = 0.1;
|
||||
var ZOOM_STEP = 0.5;
|
||||
|
||||
/**
|
||||
* This widget is deprecated, and should instead use AttachmentViewer component.
|
||||
* @see `mail/static/src/components/attachment_viewer/attachment_viewer.js`
|
||||
* TODO: remove this widget when it's not longer used
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
var DocumentViewer = Widget.extend({
|
||||
template: "DocumentViewer",
|
||||
events: {
|
||||
'click .o_download_btn': '_onDownload',
|
||||
'click .o_viewer_img': '_onImageClicked',
|
||||
'click .o_viewer_video': '_onVideoClicked',
|
||||
'click .move_next': '_onNext',
|
||||
'click .move_previous': '_onPrevious',
|
||||
'click .o_rotate': '_onRotate',
|
||||
'click .o_zoom_in': '_onZoomIn',
|
||||
'click .o_zoom_out': '_onZoomOut',
|
||||
'click .o_zoom_reset': '_onZoomReset',
|
||||
'click .o_close_btn, .o_viewer_img_wrapper': '_onClose',
|
||||
'click .o_print_btn': '_onPrint',
|
||||
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
|
||||
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
|
||||
'keydown': '_onKeydown',
|
||||
'keyup': '_onKeyUp',
|
||||
'mousedown .o_viewer_img': '_onStartDrag',
|
||||
'mousemove .o_viewer_content': '_onDrag',
|
||||
'mouseup .o_viewer_content': '_onEndDrag'
|
||||
},
|
||||
/**
|
||||
* The documentViewer takes an array of objects describing attachments in
|
||||
* argument, and the ID of an active attachment (the one to display first).
|
||||
* Documents that are not of type image or video are filtered out.
|
||||
*
|
||||
* @override
|
||||
* @param {Array<Object>} attachments list of attachments
|
||||
* @param {integer} activeAttachmentID
|
||||
*/
|
||||
init: function (parent, attachments, activeAttachmentID) {
|
||||
this._super.apply(this, arguments);
|
||||
this.attachment = _.filter(attachments, function (attachment) {
|
||||
var match = attachment.type === 'url' ? attachment.url.match("(youtu|.png|.jpg|.gif)") : attachment.mimetype.match("(image|video|application/pdf|text)");
|
||||
if (match) {
|
||||
attachment.fileType = match[1];
|
||||
if (match[1].match("(.png|.jpg|.gif)")) {
|
||||
attachment.fileType = 'image';
|
||||
}
|
||||
if (match[1] === 'youtu') {
|
||||
var youtube_array = attachment.url.split('/');
|
||||
var youtube_token = youtube_array[youtube_array.length-1];
|
||||
if (youtube_token.indexOf('watch') !== -1) {
|
||||
youtube_token = youtube_token.split('v=')[1];
|
||||
var amp = youtube_token.indexOf('&')
|
||||
if (amp !== -1){
|
||||
youtube_token = youtube_token.substring(0, amp);
|
||||
}
|
||||
}
|
||||
attachment.youtube = youtube_token;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.activeAttachment = _.findWhere(attachments, {id: activeAttachmentID});
|
||||
this.modelName = 'ir.attachment';
|
||||
this._reset();
|
||||
},
|
||||
/**
|
||||
* Do some actions after the widget is appended to the DOM
|
||||
* @override
|
||||
*/
|
||||
setElement: function () {
|
||||
const result = this._super(...arguments);
|
||||
this._hidePdfButtonsIfPresent();
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Open a modal displaying the active attachment
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$el.modal('show');
|
||||
this.$el.on('hidden.bs.modal', _.bind(this._onDestroy, this));
|
||||
this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this));
|
||||
this.$('[data-bs-toggle="tooltip"]').tooltip({delay: 0});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
this.$el.modal('hide');
|
||||
this.$el.remove();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hide some buttons in PDF.js
|
||||
* @override
|
||||
*/
|
||||
_hidePdfButtonsIfPresent: function () {
|
||||
if (this.activeAttachment.mimetype === 'application/pdf') {
|
||||
hidePDFJSButtons(this.el);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_next: function () {
|
||||
var index = _.findIndex(this.attachment, this.activeAttachment);
|
||||
index = (index + 1) % this.attachment.length;
|
||||
this.activeAttachment = this.attachment[index];
|
||||
this._updateContent();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_previous: function () {
|
||||
var index = _.findIndex(this.attachment, this.activeAttachment);
|
||||
index = index === 0 ? this.attachment.length - 1 : index - 1;
|
||||
this.activeAttachment = this.attachment[index];
|
||||
this._updateContent();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_reset: function () {
|
||||
this.scale = 1;
|
||||
this.dragStartX = this.dragstopX = 0;
|
||||
this.dragStartY = this.dragstopY = 0;
|
||||
},
|
||||
/**
|
||||
* Render the active attachment
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateContent: function () {
|
||||
this.$('.o_viewer_content').html(QWeb.render('DocumentViewer.Content', {
|
||||
widget: this
|
||||
}));
|
||||
this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this));
|
||||
this._hidePdfButtonsIfPresent();
|
||||
this.$('[data-bs-toggle="tooltip"]').tooltip({delay: 0});
|
||||
this._reset();
|
||||
},
|
||||
/**
|
||||
* Get CSS transform property based on scale and angle
|
||||
*
|
||||
* @private
|
||||
* @param {float} scale
|
||||
* @param {float} angle
|
||||
*/
|
||||
_getTransform: function(scale, angle) {
|
||||
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)';
|
||||
},
|
||||
/**
|
||||
* Rotate image clockwise by provided angle
|
||||
*
|
||||
* @private
|
||||
* @param {float} angle
|
||||
*/
|
||||
_rotate: function (angle) {
|
||||
this._reset();
|
||||
var new_angle = (this.angle || 0) + angle;
|
||||
this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle));
|
||||
this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%');
|
||||
this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%');
|
||||
this.angle = new_angle;
|
||||
},
|
||||
/**
|
||||
* Zoom in/out image by provided scale
|
||||
*
|
||||
* @private
|
||||
* @param {integer} scale
|
||||
*/
|
||||
_zoom: function (scale) {
|
||||
if (scale > 0.5) {
|
||||
this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0));
|
||||
this.scale = scale;
|
||||
}
|
||||
this.$('.o_zoom_reset').add('.o_zoom_out').toggleClass('disabled', scale === 1);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onClose: function (e) {
|
||||
e.preventDefault();
|
||||
this.destroy();
|
||||
},
|
||||
/**
|
||||
* When popup close complete destroyed modal even DOM footprint too
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onDestroy: function () {
|
||||
this.destroy();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onDownload: function (e) {
|
||||
e.preventDefault();
|
||||
window.location = '/web/content/' + this.modelName + '/' + this.activeAttachment.id + '/' + 'datas' + '?download=true';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onDrag: function (e) {
|
||||
e.preventDefault();
|
||||
if (this.enableDrag) {
|
||||
var $image = this.$('.o_viewer_img');
|
||||
var $zoomer = this.$('.o_viewer_zoomer');
|
||||
var top = $image.prop('offsetHeight') * this.scale > $zoomer.height() ? e.clientY - this.dragStartY : 0;
|
||||
var left = $image.prop('offsetWidth') * this.scale > $zoomer.width() ? e.clientX - this.dragStartX : 0;
|
||||
$zoomer.css("transform", "translate3d("+ left +"px, " + top + "px, 0)");
|
||||
$image.css('cursor', 'move');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onEndDrag: function (e) {
|
||||
e.preventDefault();
|
||||
if (this.enableDrag) {
|
||||
this.enableDrag = false;
|
||||
this.dragstopX = e.clientX - this.dragStartX;
|
||||
this.dragstopY = e.clientY - this.dragStartY;
|
||||
this.$('.o_viewer_img').css('cursor', '');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* On click of image do not close modal so stop event propagation
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onImageClicked: function (e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
/**
|
||||
* Remove loading indicator when image loaded
|
||||
* @private
|
||||
*/
|
||||
_onImageLoaded: function () {
|
||||
this.$('.o_loading_img').hide();
|
||||
},
|
||||
/**
|
||||
* Move next previous attachment on keyboard right left key
|
||||
*
|
||||
* @private
|
||||
* @param {KeyEvent} e
|
||||
*/
|
||||
_onKeydown: function (e){
|
||||
switch (e.which) {
|
||||
case $.ui.keyCode.RIGHT:
|
||||
e.preventDefault();
|
||||
this._next();
|
||||
break;
|
||||
case $.ui.keyCode.LEFT:
|
||||
e.preventDefault();
|
||||
this._previous();
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Close popup on ESCAPE keyup
|
||||
*
|
||||
* @private
|
||||
* @param {KeyEvent} e
|
||||
*/
|
||||
_onKeyUp: function (e) {
|
||||
switch (e.which) {
|
||||
case $.ui.keyCode.ESCAPE:
|
||||
e.preventDefault();
|
||||
this._onClose(e);
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onNext: function (e) {
|
||||
e.preventDefault();
|
||||
this._next();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onPrevious: function (e) {
|
||||
e.preventDefault();
|
||||
this._previous();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onPrint: function (e) {
|
||||
e.preventDefault();
|
||||
var src = this.$('.o_viewer_img').prop('src');
|
||||
var script = QWeb.render('PrintImage', {
|
||||
src: src
|
||||
});
|
||||
var printWindow = window.open('about:blank', "_new");
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(script);
|
||||
printWindow.document.close();
|
||||
},
|
||||
/**
|
||||
* Zoom image on scroll
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onScroll: function (e) {
|
||||
var scale;
|
||||
if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) {
|
||||
scale = this.scale + SCROLL_ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
} else {
|
||||
scale = this.scale - SCROLL_ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onStartDrag: function (e) {
|
||||
e.preventDefault();
|
||||
this.enableDrag = true;
|
||||
this.dragStartX = e.clientX - (this.dragstopX || 0);
|
||||
this.dragStartY = e.clientY - (this.dragstopY || 0);
|
||||
},
|
||||
/**
|
||||
* On click of video do not close modal so stop event propagation
|
||||
* and provide play/pause the video instead of quitting it
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onVideoClicked: function (e) {
|
||||
e.stopPropagation();
|
||||
var videoElement = e.target;
|
||||
if (videoElement.paused) {
|
||||
videoElement.play();
|
||||
} else {
|
||||
videoElement.pause();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onRotate: function (e) {
|
||||
e.preventDefault();
|
||||
this._rotate(90);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onZoomIn: function (e) {
|
||||
e.preventDefault();
|
||||
var scale = this.scale + ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onZoomOut: function (e) {
|
||||
e.preventDefault();
|
||||
var scale = this.scale - ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onZoomReset: function (e) {
|
||||
e.preventDefault();
|
||||
this.$('.o_viewer_zoomer').css("transform", "");
|
||||
this._zoom(1);
|
||||
},
|
||||
});
|
||||
|
||||
export default DocumentViewer;
|
||||
@@ -37,121 +37,6 @@
|
||||
.o_modal_fullscreen {
|
||||
z-index: $o-mail-thread-window-zindex + 1;
|
||||
|
||||
.o_viewer_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
/* Act as `modal-content` */
|
||||
& > div {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.o_viewer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: $o-navbar-height;
|
||||
background-color: rgba(black, 0.8);
|
||||
z-index: 1;
|
||||
color: #FFFFFF;
|
||||
|
||||
a {
|
||||
@include o-hover-text-color(rgba(map-get($theme-colors, 'light'), 0.6), white);
|
||||
}
|
||||
|
||||
.o_image_caption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
margin-inline-start: map-get($spacers, 4);
|
||||
|
||||
.o_viewer_document_name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_loading_img {
|
||||
@include o-position-absolute($top: 45%, $right: 0, $left: 0);
|
||||
}
|
||||
|
||||
.o_viewer_img_wrapper {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(black, 0.7);
|
||||
|
||||
.o_viewer_zoomer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
image-orientation: from-image; // Only supported in Firefox
|
||||
}
|
||||
|
||||
img, video {
|
||||
cursor :auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: 0.2s cubic-bezier(0, 0, 0.49, 1.6) 0s, opacity 0.15s;
|
||||
box-shadow: 0 0 40px black;
|
||||
}
|
||||
|
||||
.o_viewer_pdf {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
box-shadow: 1px 1px 20px 1px #000;
|
||||
}
|
||||
|
||||
.o_viewer_text {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
box-shadow: 1px 1px 20px 1px #000;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.o_viewer_video {
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.o_viewer_pdf, .o_viewer_text {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_viewer_toolbar {
|
||||
@include o-position-absolute($bottom: $grid-gutter-width*0.5);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
|
||||
> .btn-group {
|
||||
background-color: rgba(black, 0.4);
|
||||
}
|
||||
|
||||
.o_viewer_toolbar_btn {
|
||||
@include o-viewer-black-btn;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
@include o-position-absolute(50%, $grid-gutter-width*0.5);
|
||||
border-radius: 100%;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!--
|
||||
@param {mail.DocumentViewer} widget
|
||||
-->
|
||||
<t t-name="DocumentViewer.Content">
|
||||
<div class="o_viewer_content modal-dialog modal-fullscreen">
|
||||
<t t-set="model" t-value="widget.modelName"/>
|
||||
<div class="o_viewer-header">
|
||||
<span class="o_image_caption">
|
||||
<i class="fa fa-picture-o mr8" t-if="widget.activeAttachment.fileType == 'image'" role="img" aria-label="Image" title="Image"/>
|
||||
<i class="fa fa-file-text mr8" t-if="widget.activeAttachment.fileType == 'application/pdf'" role="img" aria-label="PDF file" title="PDF file"/>
|
||||
<i class="fa fa-video-camera mr8" t-if="widget.activeAttachment.fileType == 'video'" role="img" aria-label="Video" title="Video"/>
|
||||
<span class="o_viewer_document_name" t-esc="widget.activeAttachment.name"/>
|
||||
</span>
|
||||
<div class="flex-grow-1"/>
|
||||
<a class="o_download_btn o_document_viewer_topbar_button btn" href="#" title="Download">
|
||||
<i class="fa fa-fw fa-download" role="img" aria-label="Download"/>
|
||||
<span class="d-none d-md-inline ms-2">Download</span>
|
||||
</a>
|
||||
<a role="button" href="#" class="o_close_btn o_document_viewer_topbar_button btn" title="Close">
|
||||
<i class="fa fa-fw fa-close" role="img" aria-label="Close"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="o_viewer_img_wrapper">
|
||||
<div class="o_viewer_zoomer">
|
||||
<t t-if="widget.activeAttachment.fileType === 'image'">
|
||||
<div class="o_loading_img text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin text-gray-light fa-3x fa-fw" role="img" aria-label="Loading" title="Loading"/>
|
||||
</div>
|
||||
<t t-set="unique" t-value="widget.activeAttachment.checksum ? widget.activeAttachment.checksum.slice(-8) : ''"/>
|
||||
<img class="o_viewer_img" t-attf-src="/web/image/#{widget.activeAttachment.id}?unique=#{unique}&model=#{model}" alt="Viewer"/>
|
||||
</t>
|
||||
<iframe t-if="widget.activeAttachment.fileType == 'application/pdf'" class="o_viewer_pdf" t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/web/content/#{widget.activeAttachment.id}?model%3D#{model}%26filename%3D#{window.encodeURIComponent(widget.activeAttachment.name)}" />
|
||||
<iframe t-if="(widget.activeAttachment.fileType || '').indexOf('text') !== -1" class="o_viewer_text" t-attf-src="/web/content/#{widget.activeAttachment.id}?model=#{model}" />
|
||||
<iframe t-if="widget.activeAttachment.fileType == 'youtu'" class="o_viewer_text" allow="autoplay; encrypted-media" width="560" height="315" t-attf-src="https://www.youtube.com/embed/#{widget.activeAttachment.youtube}"/>
|
||||
<video t-if="widget.activeAttachment.fileType == 'video'" class="o_viewer_video" controls="controls">
|
||||
<source t-attf-src="/web/image/#{widget.activeAttachment.id}?model=#{model}" t-att-data-type="widget.activeAttachment.mimetype"/>
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="widget.activeAttachment.fileType == 'image'" class="o_viewer_toolbar btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_zoom_in" data-bs-toggle="tooltip" title="Zoom In"><i class="fa fa-fw fa-plus" role="img" aria-label="Zoom In"/></a>
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_zoom_reset disabled" data-bs-toggle="tooltip" title="Reset Zoom"><i class="fa fa-fw fa-search" role="img" aria-label="Reset Zoom"/></a>
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_zoom_out disabled" data-bs-toggle="tooltip" title="Zoom Out"><i class="fa fa-fw fa-minus" role="img" aria-label="Zoom Out"/></a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_rotate" data-bs-toggle="tooltip" title="Rotate"><i class="fa fa-fw fa-repeat" role="img" aria-label="Rotate"/></a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_print_btn" data-bs-toggle="tooltip" title="Print"><i class="fa fa-fw fa-print" role="img" aria-label="Print"/></a>
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_download_btn" data-bs-toggle="tooltip" title="Download"><i class="fa fa-fw fa-download" role="img" aria-label="Download"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.DocumentViewer} widget
|
||||
-->
|
||||
<t t-name="DocumentViewer">
|
||||
<div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog">
|
||||
<t class="o_document_viewer_content_call" t-call="DocumentViewer.Content"/>
|
||||
|
||||
<t t-if="widget.attachment.length !== 1">
|
||||
<a class="arrow arrow-left move_previous" href="#">
|
||||
<span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/>
|
||||
</a>
|
||||
<a class="arrow arrow-right move_next" href="#">
|
||||
<span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {string} src
|
||||
-->
|
||||
<t t-name="PrintImage">
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
function onload_img() {
|
||||
setTimeout('print_img()', 10);
|
||||
}
|
||||
function print_img() {
|
||||
window.print();
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload='onload_img()'>
|
||||
<img t-att-src='src' alt=""/>
|
||||
</body>
|
||||
</html>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,222 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import DocumentViewer from '@mail/js/document_viewer';
|
||||
|
||||
import testUtils from 'web.test_utils';
|
||||
import Widget from 'web.Widget';
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {Object[]} params.attachments
|
||||
* @param {int} params.attachmentID
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {boolean} [params.debug]
|
||||
* @returns {DocumentViewer}
|
||||
*/
|
||||
var createViewer = async function (params) {
|
||||
var parent = new Widget();
|
||||
var viewer = new DocumentViewer(parent, params.attachments, params.attachmentID);
|
||||
|
||||
var mockRPC = function (route) {
|
||||
if (route === '/web/static/lib/pdfjs/web/viewer.html?file=/web/content/1?model%3Dir.attachment%26filename%3DfilePdf.pdf') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (route === 'https://www.youtube.com/embed/FYqW0Gdwbzk') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (route === '/web/content/4?model=ir.attachment') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (route === '/web/image/6?unique=56789abc&model=ir.attachment') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
mockRPC: function () {
|
||||
if (params.mockRPC) {
|
||||
var _super = this._super;
|
||||
this._super = mockRPC;
|
||||
var def = params.mockRPC.apply(this, arguments);
|
||||
this._super = _super;
|
||||
return def;
|
||||
} else {
|
||||
return mockRPC.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
intercepts: params.intercepts || {},
|
||||
});
|
||||
var $target = $("#qunit-fixture");
|
||||
if (params.debug) {
|
||||
$target = $('body');
|
||||
$target.addClass('debug');
|
||||
}
|
||||
|
||||
// actually destroy the parent when the viewer is destroyed
|
||||
viewer.destroy = function () {
|
||||
delete viewer.destroy;
|
||||
parent.destroy();
|
||||
};
|
||||
return viewer.appendTo($target).then(function() {
|
||||
return viewer;
|
||||
});
|
||||
};
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('document_viewer_tests.js', {
|
||||
beforeEach: function () {
|
||||
this.attachments = [
|
||||
{id: 1, name: 'filePdf.pdf', type: 'binary', mimetype: 'application/pdf', datas:'R0lGOP////ywAADs='},
|
||||
{id: 2, name: 'urlYoutube', type: 'url', mimetype: '', url: 'https://youtu.be/FYqW0Gdwbzk'},
|
||||
{id: 3, name: 'urlRandom', type: 'url', mimetype: '', url: 'https://www.google.com'},
|
||||
{id: 4, name: 'text.html', type: 'binary', mimetype: 'text/html', datas:'testee'},
|
||||
{id: 5, name: 'video.mp4', type: 'binary', mimetype: 'video/mp4', datas:'R0lDOP////ywAADs='},
|
||||
{id: 6, name: 'image.jpg', type: 'binary', mimetype: 'image/jpeg', checksum: '123456789abc', datas:'R0lVOP////ywAADs='},
|
||||
];
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test('basic rendering', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 1,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.containsOnce(viewer, '.o_viewer_content',
|
||||
"there should be a preview");
|
||||
assert.containsOnce(viewer, '.o_close_btn',
|
||||
"there should be a close button");
|
||||
assert.containsOnce(viewer, '.o_viewer-header',
|
||||
"there should be a header");
|
||||
assert.containsOnce(viewer, '.o_image_caption',
|
||||
"there should be an image caption");
|
||||
assert.containsOnce(viewer, '.o_viewer_zoomer',
|
||||
"there should be a zoomer");
|
||||
assert.containsOnce(viewer, '.fa-chevron-right',
|
||||
"there should be a right nav icon");
|
||||
assert.containsOnce(viewer, '.fa-chevron-left',
|
||||
"there should be a left nav icon");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer Youtube', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 2,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('urlYoutube')").length, 1,
|
||||
"the viewer should be on the right attachment");
|
||||
assert.containsOnce(viewer, '.o_viewer_text[data-src="https://www.youtube.com/embed/FYqW0Gdwbzk"]',
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer html/(txt)', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 4,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('text.html')").length, 1,
|
||||
"the viewer be on the right attachment");
|
||||
assert.containsOnce(viewer, 'iframe[data-src="/web/content/4?model=ir.attachment"]',
|
||||
"there should be an iframe with the right src");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer mp4', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 5,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('video.mp4')").length, 1,
|
||||
"the viewer be on the right attachment");
|
||||
assert.containsOnce(viewer, '.o_viewer_video',
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer jpg', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 6,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('image.jpg')").length, 1,
|
||||
"the viewer be on the right attachment");
|
||||
assert.containsOnce(viewer, 'img[data-src="/web/image/6?unique=56789abc&model=ir.attachment"]',
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('is closable by button', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 6,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.containsOnce(viewer, '.o_viewer_content',
|
||||
"should have a document viewer");
|
||||
assert.containsOnce(viewer, '.o_close_btn',
|
||||
"should have a close button");
|
||||
|
||||
await testUtils.dom.click(viewer.$('.o_close_btn'));
|
||||
|
||||
assert.ok(viewer.isDestroyed(), 'viewer should be destroyed');
|
||||
});
|
||||
|
||||
QUnit.test('is closable by clicking on the wrapper', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 6,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.containsOnce(viewer, '.o_viewer_content',
|
||||
"should have a document viewer");
|
||||
assert.containsOnce(viewer, '.o_viewer_img_wrapper',
|
||||
"should have a wrapper");
|
||||
|
||||
await testUtils.dom.click(viewer.$('.o_viewer_img_wrapper'));
|
||||
|
||||
assert.ok(viewer.isDestroyed(), 'viewer should be destroyed');
|
||||
});
|
||||
|
||||
QUnit.test('fileType and integrity test', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 2,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(this.attachments[1].type, 'url',
|
||||
"the type should be url");
|
||||
assert.strictEqual(this.attachments[1].fileType, 'youtu',
|
||||
"there should be a fileType 'youtu'");
|
||||
assert.strictEqual(this.attachments[1].youtube, 'FYqW0Gdwbzk',
|
||||
"there should be a youtube token");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import KanbanView from 'web.KanbanView';
|
||||
import MrpDocumentsKanbanController from '@mrp/js/mrp_documents_kanban_controller';
|
||||
import MrpDocumentsKanbanRenderer from '@mrp/js/mrp_documents_kanban_renderer';
|
||||
import viewRegistry from 'web.view_registry';
|
||||
|
||||
const MrpDocumentsKanbanView = KanbanView.extend({
|
||||
config: Object.assign({}, KanbanView.prototype.config, {
|
||||
Controller: MrpDocumentsKanbanController,
|
||||
Renderer: MrpDocumentsKanbanRenderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('mrp_documents_kanban', MrpDocumentsKanbanView);
|
||||
|
||||
export default MrpDocumentsKanbanView;
|
||||
@@ -1,125 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t, qweb } from 'web.core';
|
||||
import fileUploadMixin from 'web.fileUploadMixin';
|
||||
import DocumentViewer from '@mrp/js/mrp_documents_document_viewer';
|
||||
|
||||
const MrpDocumentsControllerMixin = Object.assign({}, fileUploadMixin, {
|
||||
events: {
|
||||
'click .o_mrp_documents_kanban_upload': '_onClickMrpDocumentsUpload',
|
||||
},
|
||||
custom_events: Object.assign({}, fileUploadMixin.custom_events, {
|
||||
kanban_image_clicked: '_onKanbanPreview',
|
||||
upload_file: '_onUploadFile',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called right after the reload of the view.
|
||||
*/
|
||||
async reload() {
|
||||
await this._renderFileUploads();
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {jQueryElement} $node
|
||||
*/
|
||||
renderButtons($node) {
|
||||
if (this.is_action_enabled('edit')) {
|
||||
this.$buttons = $(qweb.render('MrpDocumentsKanbanView.buttons'));
|
||||
this.$buttons.appendTo($node);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getFileUploadRoute() {
|
||||
return '/mrp/upload_attachment';
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {integer} param0.recordId
|
||||
*/
|
||||
_makeFileUploadFormDataKeys() {
|
||||
const context = this.model.get(this.handle, { raw: true }).getContext();
|
||||
return {
|
||||
res_id: context.default_res_id,
|
||||
res_model: context.default_res_model,
|
||||
};
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onClickMrpDocumentsUpload() {
|
||||
const $uploadInput = $('<input>', {
|
||||
type: 'file',
|
||||
name: 'files[]',
|
||||
multiple: 'multiple'
|
||||
});
|
||||
$uploadInput.on('change', async ev => {
|
||||
await this._uploadFiles(ev.target.files);
|
||||
$uploadInput.remove();
|
||||
});
|
||||
$uploadInput.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles custom event to display the document viewer.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {integer} ev.data.recordID
|
||||
* @param {Array<Object>} ev.data.recordList
|
||||
*/
|
||||
_onKanbanPreview(ev) {
|
||||
ev.stopPropagation();
|
||||
const documents = ev.data.recordList;
|
||||
const documentID = ev.data.recordID;
|
||||
const documentViewer = new DocumentViewer(this, documents, documentID);
|
||||
documentViewer.appendTo(this.$('.o_mrp_documents_kanban_view'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Specially created to call `_uploadFiles` method from tests.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
async _onUploadFile(ev) {
|
||||
await this._uploadFiles(ev.data.files);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} param0
|
||||
* @param {XMLHttpRequest} param0.xhr
|
||||
*/
|
||||
_onUploadLoad({ xhr }) {
|
||||
const result = xhr.status === 200
|
||||
? JSON.parse(xhr.response)
|
||||
: {
|
||||
error: _.str.sprintf(_t("status code: %s, message: %s"), xhr.status, xhr.response)
|
||||
};
|
||||
if (result.error) {
|
||||
this.displayNotification({ title: _t("Error"), message: result.error, sticky: true });
|
||||
}
|
||||
fileUploadMixin._onUploadLoad.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
export default MrpDocumentsControllerMixin;
|
||||
@@ -1,15 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import DocumentViewer from '@mail/js/document_viewer';
|
||||
|
||||
/**
|
||||
* This file defines the DocumentViewer for the MRP Documents Kanban view.
|
||||
*/
|
||||
const MrpDocumentsDocumentViewer = DocumentViewer.extend({
|
||||
init(parent, attachments, activeAttachmentID) {
|
||||
this._super(...arguments);
|
||||
this.modelName = 'mrp.document';
|
||||
},
|
||||
});
|
||||
|
||||
export default MrpDocumentsDocumentViewer;
|
||||
@@ -1,34 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* This file defines the Controller for the MRP Documents Kanban view, which is an
|
||||
* override of the KanbanController.
|
||||
*/
|
||||
|
||||
import MrpDocumentsControllerMixin from '@mrp/js/mrp_documents_controller_mixin';
|
||||
|
||||
import KanbanController from 'web.KanbanController';
|
||||
|
||||
const MrpDocumentsKanbanController = KanbanController.extend(MrpDocumentsControllerMixin, {
|
||||
events: Object.assign({}, KanbanController.prototype.events, MrpDocumentsControllerMixin.events),
|
||||
custom_events: Object.assign({}, KanbanController.prototype.custom_events, MrpDocumentsControllerMixin.custom_events),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
MrpDocumentsControllerMixin.init.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Override to update the records selection.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
async reload() {
|
||||
await this._super(...arguments);
|
||||
await MrpDocumentsControllerMixin.reload.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
export default MrpDocumentsKanbanController;
|
||||
@@ -1,47 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* This file defines the KanbanRecord for the MRP Documents Kanban view.
|
||||
*/
|
||||
|
||||
import KanbanRecord from 'web.KanbanRecord';
|
||||
|
||||
const MrpDocumentsKanbanRecord = KanbanRecord.extend({
|
||||
events: Object.assign({}, KanbanRecord.prototype.events, {
|
||||
'click .o_mrp_download': '_onDownload',
|
||||
'click .o_kanban_previewer': '_onImageClicked',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles the click on the download link to save the attachment locally.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onDownload(ev) {
|
||||
ev.preventDefault();
|
||||
window.location = `/web/content/${this.modelName}/${this.id}/datas?download=true`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the click on the preview image. Triggers up `_onKanbanPreview` to
|
||||
* display `DocumentViewer`.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onImageClicked(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('kanban_image_clicked', {
|
||||
recordList: [this.recordData],
|
||||
recordID: this.recordData.id
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default MrpDocumentsKanbanRecord;
|
||||
@@ -1,24 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* This file defines the Renderer for the MRP Documents Kanban view, which is an
|
||||
* override of the KanbanRenderer.
|
||||
*/
|
||||
|
||||
import KanbanRenderer from 'web.KanbanRenderer';
|
||||
import MrpDocumentsKanbanRecord from '@mrp/js/mrp_documents_kanban_record';
|
||||
|
||||
const MrpDocumentsKanbanRenderer = KanbanRenderer.extend({
|
||||
config: Object.assign({}, KanbanRenderer.prototype.config, {
|
||||
KanbanRecord: MrpDocumentsKanbanRecord,
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async start() {
|
||||
this.$el.addClass('o_mrp_documents_kanban_view');
|
||||
await this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
export default MrpDocumentsKanbanRenderer;
|
||||
@@ -0,0 +1,40 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { useRef } = owl;
|
||||
|
||||
export class MrpDocumentsKanbanController extends KanbanController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.uploadFileInputRef = useRef("uploadFileInput");
|
||||
this.fileUploadService = useService("file_upload");
|
||||
useBus(
|
||||
this.fileUploadService.bus,
|
||||
"FILE_UPLOAD_LOADED",
|
||||
async () => {
|
||||
await this.model.root.load();
|
||||
this.model.notify();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async onFileInputChange(ev) {
|
||||
if (!ev.target.files.length) {
|
||||
return;
|
||||
}
|
||||
await this.fileUploadService.upload(
|
||||
"/mrp/upload_attachment",
|
||||
ev.target.files,
|
||||
{
|
||||
buildFormData: (formData) => {
|
||||
formData.append("res_model", this.props.context.default_res_model);
|
||||
formData.append("res_id", this.props.context.default_res_id);
|
||||
},
|
||||
},
|
||||
);
|
||||
// Reset the file input's value so that the same file may be uploaded twice.
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mrp.MrpDocumentsKanbanView.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<div role="toolbar" position="inside">
|
||||
<input type="file" multiple="true" t-ref="uploadFileInput" class="o_input_file o_hidden" t-on-change.stop="onFileInputChange"/>
|
||||
<button type="button" t-attf-class="btn btn-primary o_mrp_documents_kanban_upload"
|
||||
t-on-click.stop.prevent="() => this.uploadFileInputRef.el.click()">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,41 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { CANCEL_GLOBAL_CLICK, KanbanRecord } from "@web/views/kanban/kanban_record";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class MrpDocumentsKanbanRecord extends KanbanRecord {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.messaging = useService("messaging");
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Override to open the preview upon clicking the image, if compatible.
|
||||
*/
|
||||
onGlobalClick(ev) {
|
||||
if (ev.target.closest(CANCEL_GLOBAL_CLICK) && !ev.target.classList.contains("o_mrp_download")) {
|
||||
return;
|
||||
}
|
||||
if (ev.target.classList.contains("o_mrp_download")) {
|
||||
window.location = `/web/content/mrp.document/${this.props.record.resId}/datas?download=true`;
|
||||
return;
|
||||
} else if (ev.target.closest(".o_kanban_previewer")) {
|
||||
this.messaging.get().then((messaging) => {
|
||||
const attachmentList = messaging.models["AttachmentList"].insert({
|
||||
selectedAttachment: messaging.models["Attachment"].insert({
|
||||
id: this.props.record.data.ir_attachment_id[0],
|
||||
filename: this.props.record.data.name,
|
||||
name: this.props.record.data.name,
|
||||
mimetype: this.props.record.data.mimetype,
|
||||
}),
|
||||
});
|
||||
this.dialog = messaging.models["Dialog"].insert({
|
||||
attachmentListOwnerAsAttachmentView: attachmentList,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
return super.onGlobalClick(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { MrpDocumentsKanbanRecord } from "@mrp/views/mrp_documents_kanban/mrp_documents_kanban_record";
|
||||
import { FileUploadProgressContainer } from "@web/core/file_upload/file_upload_progress_container";
|
||||
import { FileUploadProgressKanbanRecord } from "@web/core/file_upload/file_upload_progress_record";
|
||||
|
||||
export class MrpDocumentsKanbanRenderer extends KanbanRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.fileUploadService = useService("file_upload");
|
||||
}
|
||||
}
|
||||
|
||||
MrpDocumentsKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
FileUploadProgressContainer,
|
||||
FileUploadProgressKanbanRecord,
|
||||
KanbanRecord: MrpDocumentsKanbanRecord,
|
||||
};
|
||||
MrpDocumentsKanbanRenderer.template = "mrp.MrpDocumentsKanbanRenderer";
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mrp.MrpDocumentsKanbanRenderer" owl="1" t-inherit-mode="primary" t-inherit="web.KanbanRenderer">
|
||||
<!-- Before the first t-foreach -->
|
||||
<xpath expr="//t[@t-key='groupOrRecord.key']" position="before">
|
||||
<FileUploadProgressContainer fileUploads="fileUploadService.uploads" Component="constructor.components.FileUploadProgressKanbanRecord"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { MrpDocumentsKanbanController } from "@mrp/views/mrp_documents_kanban/mrp_documents_kanban_controller";
|
||||
import { MrpDocumentsKanbanRenderer } from "@mrp/views/mrp_documents_kanban/mrp_documents_kanban_renderer";
|
||||
|
||||
export const mrpDocumentsKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: MrpDocumentsKanbanController,
|
||||
Renderer: MrpDocumentsKanbanRenderer,
|
||||
buttonTemplate: "mrp.MrpDocumentsKanbanView.Buttons",
|
||||
};
|
||||
|
||||
registry.category("views").add("mrp_documents_kanban", mrpDocumentsKanbanView);
|
||||
@@ -1,18 +1,38 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import MrpDocumentsKanbanView from '@mrp/js/mrp_document_kanban_view';
|
||||
import MrpDocumentsKanbanController from '@mrp/js/mrp_documents_kanban_controller';
|
||||
import testUtils from 'web.test_utils';
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
nextTick,
|
||||
} from '@web/../tests/helpers/utils';
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
const createView = testUtils.createView;
|
||||
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
|
||||
|
||||
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
addModelNamesToFetch([
|
||||
'mrp.document',
|
||||
]);
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let pyEnv;
|
||||
|
||||
QUnit.module('Views', {}, function () {
|
||||
|
||||
QUnit.module('MrpDocumentsKanbanView', {
|
||||
beforeEach: function () {
|
||||
this.ORIGINAL_CREATE_XHR = MrpDocumentsKanbanController.prototype._createXHR;
|
||||
beforeEach: async function () {
|
||||
serviceRegistry.add("file_upload", fileUploadService);
|
||||
this.ORIGINAL_CREATE_XHR = fileUploadService.createXhr;
|
||||
this.patchDocumentXHR = (mockedXHRs, customSend) => {
|
||||
MrpDocumentsKanbanController.prototype._createXhr = () => {
|
||||
fileUploadService.createXhr = () => {
|
||||
const xhr = {
|
||||
upload: new window.EventTarget(),
|
||||
open() { },
|
||||
@@ -22,54 +42,51 @@ QUnit.module('MrpDocumentsKanbanView', {
|
||||
return xhr;
|
||||
};
|
||||
};
|
||||
this.data = {
|
||||
'mrp.document': {
|
||||
fields: {
|
||||
name: {string: "Name", type: 'char', default: ' '},
|
||||
priority: {string: 'priority', type: 'selection',
|
||||
selection: [['0', 'Normal'], ['1', 'Low'], ['2', 'High'], ['3', 'Very High']]},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: 'test1', priority: 2},
|
||||
{id: 4, name: 'test2', priority: 1},
|
||||
{id: 3, name: 'test3', priority: 3},
|
||||
],
|
||||
},
|
||||
};
|
||||
pyEnv = await startServer();
|
||||
const irAttachment = pyEnv['ir.attachment'].create({
|
||||
mimetype: 'image/png',
|
||||
name: 'test.png',
|
||||
})
|
||||
pyEnv['mrp.document'].create([
|
||||
{name: 'test1', priority: 2, ir_attachment_id: irAttachment},
|
||||
{name: 'test2', priority: 1},
|
||||
{name: 'test3', priority: 3},
|
||||
]);
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
},
|
||||
afterEach() {
|
||||
MrpDocumentsKanbanController.prototype._createXHR = this.ORIGINAL_CREATE_XHR;
|
||||
fileUploadService.createXhr = this.ORIGINAL_CREATE_XHR;
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('MRP documents kanban basic rendering', async function (assert) {
|
||||
assert.expect(6);
|
||||
assert.expect(4);
|
||||
|
||||
const kanban = await createView({
|
||||
View: MrpDocumentsKanbanView,
|
||||
model: 'mrp.document',
|
||||
data: this.data,
|
||||
arch: '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
assert.ok(kanban, "kanban is created");
|
||||
assert.ok(kanban.$buttons.find('.o_mrp_documents_kanban_upload'),
|
||||
assert.ok(target.querySelector('.o_mrp_documents_kanban_upload'),
|
||||
"should have upload button in kanban buttons");
|
||||
assert.containsN(kanban, '.o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 3,
|
||||
assert.containsN(target, '.o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)', 3,
|
||||
"should have 3 records in the renderer");
|
||||
// check view layout
|
||||
assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_mrp_documents_kanban_view',
|
||||
"should have classname 'o_mrp_documents_kanban_view'");
|
||||
// check control panel buttons
|
||||
assert.containsN(kanban, '.o_cp_buttons .btn-primary', 1,
|
||||
assert.containsN(target, '.o_cp_buttons .btn-primary', 1,
|
||||
"should have only 1 primary button i.e. Upload button");
|
||||
assert.strictEqual(kanban.$('.o_cp_buttons .btn-primary:first').text().trim(), 'Upload',
|
||||
assert.equal(target.querySelector(".o_cp_buttons .btn-primary").innerText.trim().toUpperCase(), 'UPLOAD',
|
||||
"should have a primary 'Upload' button");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('mrp: upload multiple files', async function (assert) {
|
||||
@@ -94,26 +111,36 @@ QUnit.module('MrpDocumentsKanbanView', {
|
||||
const mockedXHRs = [];
|
||||
this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
|
||||
|
||||
const kanban = await createView({
|
||||
View: MrpDocumentsKanbanView,
|
||||
model: 'mrp.document',
|
||||
data: this.data,
|
||||
arch: '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
kanban.trigger_up('upload_file', {files: [file1]});
|
||||
await testUtils.nextTick();
|
||||
const fileInput = target.querySelector(".o_input_file");
|
||||
|
||||
let dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file1);
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.verifySteps(['xhrSend']);
|
||||
|
||||
kanban.trigger_up('upload_file', {files: [file2, file3]});
|
||||
await testUtils.nextTick();
|
||||
dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file2);
|
||||
dataTransfer.items.add(file3);
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.verifySteps(['xhrSend']);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('mrp: upload progress bars', async function (assert) {
|
||||
@@ -128,19 +155,28 @@ QUnit.module('MrpDocumentsKanbanView', {
|
||||
const mockedXHRs = [];
|
||||
this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
|
||||
|
||||
const kanban = await createView({
|
||||
View: MrpDocumentsKanbanView,
|
||||
model: 'mrp.document',
|
||||
data: this.data,
|
||||
arch: '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
kanban.trigger_up('upload_file', {files: [file1]});
|
||||
await testUtils.nextTick();
|
||||
const fileInput = target.querySelector(".o_input_file");
|
||||
|
||||
let dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file1);
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.verifySteps(['xhrSend']);
|
||||
|
||||
const progressEvent = new Event('progress', { bubbles: true });
|
||||
@@ -148,21 +184,60 @@ QUnit.module('MrpDocumentsKanbanView', {
|
||||
progressEvent.total = 500000000;
|
||||
progressEvent.lengthComputable = true;
|
||||
mockedXHRs[0].upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
kanban.$('.o_file_upload_progress_text_left').text(),
|
||||
target.querySelector('.o_file_upload_progress_text_left').innerText,
|
||||
"Uploading... (50%)",
|
||||
"the current upload progress should be at 50%"
|
||||
);
|
||||
|
||||
|
||||
progressEvent.loaded = 350000000;
|
||||
mockedXHRs[0].upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
kanban.$('.o_file_upload_progress_text_right').text(),
|
||||
"(350/500Mb)",
|
||||
target.querySelector('.o_file_upload_progress_text_right').innerText,
|
||||
"(350/500MB)",
|
||||
"the current upload progress should be at (350/500Mb)"
|
||||
);
|
||||
});
|
||||
|
||||
kanban.destroy();
|
||||
QUnit.test("mrp: click on image opens attachment viewer", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div class="o_kanban_image" t-if="record.ir_attachment_id.raw_value">
|
||||
<div class="o_kanban_previewer">
|
||||
<field name="ir_attachment_id" invisible="1"/>
|
||||
<img t-attf-src="/web/image/#{record.ir_attachment_id.raw_value}" width="100" height="100" alt="Document" class="o_attachment_image"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_previewer");
|
||||
await click(target.querySelector(".o_kanban_previewer"));
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, '.o_AttachmentViewer',
|
||||
"should have a document preview");
|
||||
assert.containsOnce(target, '.o_AttachmentViewer_headerItemButtonClose',
|
||||
"should have a close button");
|
||||
|
||||
await click(target, '.o_AttachmentViewer_headerItemButtonClose');
|
||||
assert.containsNone(target, '.o_AttachmentViewer',
|
||||
"should not have a document preview");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="name">mrp.document kanban.mrp</field>
|
||||
<field name="model">mrp.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="mrp_documents_kanban" create="false">
|
||||
<kanban js_class="mrp_documents_kanban" class="o_mrp_documents_kanban_view" create="false">
|
||||
<field name="ir_attachment_id"/>
|
||||
<field name="mimetype"/>
|
||||
<field name="type"/>
|
||||
|
||||
@@ -46,8 +46,6 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/xml/control_panel.xml',
|
||||
'web/static/src/legacy/xml/chart.xml',
|
||||
'web/static/src/legacy/xml/fields.xml',
|
||||
'web/static/src/legacy/xml/file_upload_progress_bar.xml',
|
||||
'web/static/src/legacy/xml/file_upload_progress_card.xml',
|
||||
'web/static/src/legacy/xml/kanban.xml',
|
||||
'web/static/src/legacy/xml/pivot.xml',
|
||||
'web/static/src/legacy/xml/web_calendar.xml',
|
||||
@@ -122,7 +120,6 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/scss/switch_company_menu.scss',
|
||||
'web/static/src/legacy/scss/ace.scss',
|
||||
'web/static/src/legacy/scss/fields.scss',
|
||||
'web/static/src/legacy/scss/file_upload.scss',
|
||||
'web/static/src/legacy/scss/views.scss',
|
||||
'web/static/src/legacy/scss/form_view.scss',
|
||||
'web/static/src/legacy/scss/list_view.scss',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "../utils/hooks";
|
||||
import { sprintf } from "../utils/strings";
|
||||
import { ConfirmationDialog } from "../confirmation_dialog/confirmation_dialog";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class FileUploadProgressBar extends Component {
|
||||
setup() {
|
||||
this.dialogService = useService("dialog");
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
if (!this.props.fileUpload.xhr) {
|
||||
return;
|
||||
}
|
||||
this.dialogService.add(ConfirmationDialog, {
|
||||
body: sprintf(this.env._t("Do you really want to cancel the upload of %s?"), this.props.fileUpload.title),
|
||||
confirm: () => {
|
||||
this.props.fileUpload.xhr.abort();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
FileUploadProgressBar.props = {
|
||||
fileUpload: { type: Object },
|
||||
};
|
||||
FileUploadProgressBar.template = "web.FileUploadProgressBar";
|
||||
@@ -0,0 +1,19 @@
|
||||
.o-file-upload-progress-bar-value {
|
||||
transition: width 0.1s;
|
||||
border-right: 1px solid darken($o-brand-primary, 7.5%);
|
||||
background-color: $o-brand-primary;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.o-file-upload-progress-bar-abort {
|
||||
padding: 4px;
|
||||
color: #963535;
|
||||
font-size: 16px;
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
div:not(:hover) .o-file-upload-progress-bar-abort {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="web.FileUploadProgressBar" owl="1">
|
||||
<div class="position-absolute top-0 start-0 h-100 w-100">
|
||||
<div class="o-file-upload-progress-bar-value h-100" t-ref="bar" t-att-style="'width: ' + (this.props.fileUpload.progress * 100) + '%;'"/>
|
||||
<span class="position-absolute top-0 end-0 cursor-pointer o-file-upload-progress-bar-abort fa fa-times-circle" title="Cancel Upload" aria-label="Cancel Upload" t-on-click.stop.prevent="onCancel"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class FileUploadProgressContainer extends Component {}
|
||||
FileUploadProgressContainer.template = "web.FileUploadProgressContainer";
|
||||
FileUploadProgressContainer.props = {
|
||||
Component: { optional: false },
|
||||
shouldDisplay: { type: Function, optional: true },
|
||||
fileUploads: { type: Object },
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="web.FileUploadProgressContainer" owl="1">
|
||||
<t t-foreach="Object.values(props.fileUploads)" t-as="fileUpload" t-key="fileUpload.id">
|
||||
<t t-if="!props.shouldDisplay || props.shouldDisplay(fileUpload)" t-component="props.Component" fileUpload="fileUpload" selector="props.selector"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,35 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { sprintf } from "../utils/strings";
|
||||
import { FileUploadProgressBar } from "./file_upload_progress_bar";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class FileUploadProgressRecord extends Component {
|
||||
getProgressTexts() {
|
||||
const fileUpload = this.props.fileUpload;
|
||||
const percent = Math.round(fileUpload.progress * 100);
|
||||
if (percent === 100) {
|
||||
return {
|
||||
left: this.env._t("Processing..."),
|
||||
right: "",
|
||||
};
|
||||
} else {
|
||||
const mbLoaded = Math.round(fileUpload.loaded / 1000000);
|
||||
const mbTotal = Math.round(fileUpload.total / 1000000);
|
||||
return {
|
||||
left: sprintf(this.env._t("Uploading... (%s%)"), percent),
|
||||
right: sprintf(this.env._t("(%s/%sMB)"), mbLoaded, mbTotal),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
FileUploadProgressRecord.components = {
|
||||
FileUploadProgressBar,
|
||||
};
|
||||
|
||||
export class FileUploadProgressKanbanRecord extends FileUploadProgressRecord {}
|
||||
FileUploadProgressKanbanRecord.template = "web.FileUploadProgressKanbanRecord";
|
||||
|
||||
export class FileUploadProgressDataRow extends FileUploadProgressRecord {}
|
||||
FileUploadProgressDataRow.template = "web.FileUploadProgressDataRow";
|
||||
@@ -0,0 +1,23 @@
|
||||
.o_kanban_record {
|
||||
.o_kanban_progress_card {
|
||||
min-height: 80px;
|
||||
|
||||
.o_kanban_record_bottom {
|
||||
color: $gray-900;
|
||||
}
|
||||
.o_kanban_image_wrapper {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_data_row.o_list_progress_card {
|
||||
height: 25px;
|
||||
border: 1px solid #dfdfdf;
|
||||
|
||||
.o_file_upload_upload_title {
|
||||
color: $headings-color;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="web.FileUploadProgressKanbanRecord" owl="1">
|
||||
<t t-set="progressTexts" t-value="getProgressTexts()"/>
|
||||
<div class="o_kanban_record d-flex flex-grow-1 flex-md-shrink-1 flex-shrink-0">
|
||||
<div class="o_kanban_progress_card o_kanban_attachment position-relative p-0 cursor-pointer">
|
||||
<div class="o_kanban_image">
|
||||
<div class="o_kanban_image_wrapper">
|
||||
<div class="o_image o_image_thumbnail" t-att-data-mimetype="props.fileUpload.type"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_details">
|
||||
<div class="o_kanban_details_wrapper">
|
||||
<div t-att-title="props.fileUpload.title" t-att-aria-label="props.fileUpload.title" class="o_kanban_record_title">
|
||||
<span t-esc="props.fileUpload.title"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_body"/>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<div class="o_file_upload_progress_text_left" t-esc="progressTexts.left"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span class="o_file_upload_progress_text_right" t-esc="progressTexts.right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileUploadProgressBar fileUpload="props.fileUpload"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.FileUploadProgressDataRow" owl="1">
|
||||
<t t-set="progressTexts" t-value="getProgressTexts()"/>
|
||||
<div class="o_data_row o_list_progress_card position-relative align-middle p-0">
|
||||
<span class="o_file_upload_upload_title" t-esc="props.fileUpload.title"/>
|
||||
<span class="o_file_upload_progress_text_left" t-esc="progressTexts.left"/>
|
||||
<span class="o_file_upload_progress_text_right" t-esc="progressTexts.right"/>
|
||||
<FileUploadProgressBar fileUpload="props.fileUpload"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,104 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "../registry";
|
||||
import { sprintf } from "../utils/strings";
|
||||
|
||||
const { EventBus, reactive } = owl;
|
||||
|
||||
export const fileUploadService = {
|
||||
dependencies: ["notification"],
|
||||
/**
|
||||
* Overridden during tests to return a mocked XHR.
|
||||
*
|
||||
* @private
|
||||
* @returns {XMLHttpRequest}
|
||||
*/
|
||||
createXhr() {
|
||||
return new window.XMLHttpRequest();
|
||||
},
|
||||
|
||||
start(env, { notificationService }) {
|
||||
const uploads = reactive({});
|
||||
let nextId = 1;
|
||||
const bus = new EventBus();
|
||||
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {FileList} files
|
||||
* @param {Object} [params]
|
||||
* @param {function(formData): void} [params.buildFormData]
|
||||
* @param {Boolean} [params.displayErrorNotification]
|
||||
* @returns {reactive} upload
|
||||
* @returns {XMLHttpRequest} upload.xhr
|
||||
* @returns {FormData} upload.data
|
||||
* @returns {Number} upload.progress
|
||||
* @returns {Number} upload.loaded
|
||||
* @returns {Number} upload.total
|
||||
* @returns {String} upload.title
|
||||
* @returns {String||undefined} upload.type
|
||||
*/
|
||||
const upload = async (route, files, params = {}) => {
|
||||
const xhr = this.createXhr();
|
||||
xhr.open("POST", route);
|
||||
const formData = new FormData();
|
||||
formData.append("csrf_token", odoo.csrf_token);
|
||||
for (const file of files) {
|
||||
formData.append("ufile", file);
|
||||
}
|
||||
if (params.buildFormData) {
|
||||
params.buildFormData(formData);
|
||||
}
|
||||
const upload = reactive({
|
||||
id: nextId++,
|
||||
xhr,
|
||||
data: formData,
|
||||
progress: 0,
|
||||
loaded: 0,
|
||||
total: 0,
|
||||
state: "pending",
|
||||
title: files.length === 1 ? files[0].name : sprintf(env._t("%s Files"), files.length),
|
||||
type: files.length === 1 ? files[0].type : undefined,
|
||||
});
|
||||
uploads[upload.id] = upload;
|
||||
// Progress listener
|
||||
xhr.upload.addEventListener("progress", async (ev) => {
|
||||
upload.progress = ev.loaded / ev.total;
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
upload.state = "loading";
|
||||
});
|
||||
// Load listener
|
||||
xhr.upload.addEventListener("load", async () => {
|
||||
delete uploads[upload.id];
|
||||
upload.state = "loaded";
|
||||
bus.trigger("FILE_UPLOAD_LOADED", { upload });
|
||||
});
|
||||
// Error listener
|
||||
xhr.upload.addEventListener("error", async () => {
|
||||
delete uploads[upload.id];
|
||||
upload.state = "error";
|
||||
// Disable this option if you need more explicit error handling.
|
||||
if (params.displayErrorNotification !== undefined && params.displayErrorNotification) {
|
||||
notificationService.add(env._t("An error occured while uploading."), {
|
||||
title: env._t("Error"),
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
bus.trigger("FILE_UPLOAD_ERROR", { upload });
|
||||
});
|
||||
// Abort listener, considered as error
|
||||
xhr.upload.addEventListener("abort", async () => {
|
||||
delete uploads[upload.id];
|
||||
upload.state = "abort";
|
||||
bus.trigger("FILE_UPLOAD_ERROR", { upload });
|
||||
});
|
||||
xhr.send(formData);
|
||||
bus.trigger("FILE_UPLOAD_ADDED", { upload });
|
||||
return upload;
|
||||
};
|
||||
|
||||
return { bus, upload, uploads };
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("file_upload", fileUploadService);
|
||||
@@ -1,231 +0,0 @@
|
||||
/** @odoo-module alias=web.fileUploadMixin **/
|
||||
|
||||
/**
|
||||
* Mixin to be used in view Controllers to manage uploads and generate progress bars.
|
||||
* supported views: kanban, list
|
||||
*/
|
||||
|
||||
import { csrf_token, _t } from 'web.core';
|
||||
import ProgressBar from 'web.ProgressBar';
|
||||
import ProgressCard from 'web.ProgressCard';
|
||||
|
||||
const ProgressBarMixin = {
|
||||
|
||||
custom_events: {
|
||||
progress_bar_abort: '_onProgressBarAbort',
|
||||
},
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Contains the uploads currently happening, used to attach progress bars.
|
||||
* e.g: {'fileUploadId45': {progressBar, progressCard, ...params}}
|
||||
*/
|
||||
this._fileUploads = {};
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* used to use a mocked version of Xhr in the tests.
|
||||
*
|
||||
* @private
|
||||
* @returns {XMLHttpRequest}
|
||||
*/
|
||||
_createXhr() {
|
||||
return new window.XMLHttpRequest();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getFileUploadRenderOptions() {
|
||||
return {
|
||||
predicate: () => true,
|
||||
targetCallback: undefined,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {string} upload route
|
||||
*/
|
||||
_getFileUploadRoute() {
|
||||
return '/web/binary/upload_attachment';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @param {Object[]} params.files
|
||||
* @param {XMLHttpRequest} params.xhr
|
||||
*/
|
||||
_makeFileUpload(params) {
|
||||
const { files, xhr } = params;
|
||||
const fileUploadId = _.uniqueId('fileUploadId');
|
||||
const formData = new FormData();
|
||||
const formDataKeys = this._makeFileUploadFormDataKeys(Object.assign({ fileUploadId }, params));
|
||||
|
||||
formData.append('csrf_token', csrf_token);
|
||||
for (const key in formDataKeys) {
|
||||
if (formDataKeys[key] !== undefined) {
|
||||
formData.append(key, formDataKeys[key]);
|
||||
}
|
||||
}
|
||||
for (const file of files) {
|
||||
formData.append('ufile', file);
|
||||
}
|
||||
|
||||
return {
|
||||
fileUploadId,
|
||||
xhr,
|
||||
title: files.length === 1
|
||||
? files[0].name
|
||||
: _.str.sprintf(_t("%s Files"), files.length),
|
||||
type: files.length === 1 ? files[0].type : undefined,
|
||||
formData,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.fileUploadId
|
||||
* @returns {Object} the list of the form entries of a file upload.
|
||||
*/
|
||||
_makeFileUploadFormDataKeys({ fileUploadId }) {
|
||||
return {
|
||||
callback: fileUploadId,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} fileUploadId
|
||||
*/
|
||||
async _removeFileUpload(fileUploadId) {
|
||||
const upload = this._fileUploads[fileUploadId];
|
||||
upload.progressCard && upload.progressCard.destroy();
|
||||
upload.progressBar && upload.progressBar.destroy();
|
||||
delete this._fileUploads[fileUploadId];
|
||||
await this.reload();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _renderFileUploads() {
|
||||
const { predicate, targetCallback } = this._getFileUploadRenderOptions();
|
||||
|
||||
for (const fileUploadId in this._fileUploads) {
|
||||
const upload = this._fileUploads[fileUploadId];
|
||||
if (!predicate(upload)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!upload.progressBar) {
|
||||
if (!upload.recordId || this.viewType !== 'kanban') {
|
||||
upload.progressCard = new ProgressCard(this, {
|
||||
title: upload.title,
|
||||
type: upload.type,
|
||||
viewType: this.viewType,
|
||||
});
|
||||
}
|
||||
upload.progressBar = new ProgressBar(this, {
|
||||
xhr: upload.xhr,
|
||||
title: upload.title,
|
||||
fileUploadId,
|
||||
});
|
||||
}
|
||||
|
||||
let $targetCard;
|
||||
if (upload.progressCard) {
|
||||
await upload.progressCard.prependTo(this.renderer.$el);
|
||||
$targetCard = upload.progressCard.$el;
|
||||
} else if (targetCallback) {
|
||||
$targetCard = targetCallback(upload);
|
||||
}
|
||||
await upload.progressBar.appendTo($targetCard);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object[]} files
|
||||
* @param {Object} [params] optional additional data
|
||||
*/
|
||||
async _uploadFiles(files, params={}) {
|
||||
if (!files || !files.length) { return; }
|
||||
|
||||
await new Promise(resolve => {
|
||||
const xhr = this._createXhr();
|
||||
xhr.open('POST', this._getFileUploadRoute());
|
||||
const fileUploadData = this._makeFileUpload(Object.assign({ files, xhr }, params));
|
||||
const { fileUploadId, formData } = fileUploadData;
|
||||
this._fileUploads[fileUploadId] = fileUploadData;
|
||||
xhr.upload.addEventListener("progress", ev => {
|
||||
this._updateFileUploadProgress(fileUploadId, ev);
|
||||
});
|
||||
const progressPromise = this._onBeforeUpload();
|
||||
xhr.onload = async () => {
|
||||
await progressPromise;
|
||||
resolve();
|
||||
this._onUploadLoad({ fileUploadId, xhr });
|
||||
};
|
||||
xhr.onerror = async () => {
|
||||
await progressPromise;
|
||||
resolve();
|
||||
this._onUploadError({ fileUploadId, xhr });
|
||||
};
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} fileUploadId
|
||||
* @param {ProgressEvent} ev
|
||||
*/
|
||||
_updateFileUploadProgress(fileUploadId, ev) {
|
||||
const { progressCard, progressBar } = this._fileUploads[fileUploadId];
|
||||
progressCard && progressCard.update(ev.loaded, ev.total);
|
||||
progressBar && progressBar.update(ev.loaded, ev.total);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook to customize the behaviour of _uploadFiles() before an upload is made.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _onBeforeUpload() {
|
||||
await this._renderFileUploads();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {integer} ev.data.fileUploadId
|
||||
*/
|
||||
_onProgressBarAbort(ev) {
|
||||
this._removeFileUpload(ev.data.fileUploadId);
|
||||
},
|
||||
/**
|
||||
* Hook to customize the behaviour of the xhr.onload of an upload.
|
||||
*
|
||||
* @private
|
||||
* @param {string} param0.fileUploadId
|
||||
*/
|
||||
_onUploadLoad({ fileUploadId }) {
|
||||
this._removeFileUpload(fileUploadId);
|
||||
},
|
||||
/**
|
||||
* Hook to customize the behaviour of the xhr.onerror of an upload.
|
||||
*
|
||||
* @private
|
||||
* @param {string} param1.fileUploadId
|
||||
* @param {XMLHttpRequest} param0.xhr
|
||||
*/
|
||||
_onUploadError({ fileUploadId, xhr }) {
|
||||
this.displayNotification({ title: xhr.status, message: _.str.sprintf(_t('message: %s'), xhr.reponseText), sticky: true });
|
||||
this._removeFileUpload(fileUploadId);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default ProgressBarMixin;
|
||||
@@ -1,73 +0,0 @@
|
||||
/** @odoo-module alias=web.ProgressBar **/
|
||||
|
||||
import { _t } from 'web.core';
|
||||
import Dialog from 'web.Dialog';
|
||||
import Widget from 'web.Widget';
|
||||
|
||||
const ProgressBar = Widget.extend({
|
||||
template: 'web.FileUploadProgressBar',
|
||||
|
||||
events: {
|
||||
'click .o_upload_cross': '_onClickCross',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} param1
|
||||
* @param {String} param1.title
|
||||
* @param {String} param1.fileUploadId
|
||||
* @param {XMLHttpRequest} param2.xhr
|
||||
*/
|
||||
init(parent, { title, fileUploadId, xhr }) {
|
||||
this._super(...arguments);
|
||||
this.title = title;
|
||||
this.fileUploadId = fileUploadId;
|
||||
this.xhr = xhr;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {Promise}
|
||||
*/
|
||||
start() {
|
||||
this.xhr.onabort = () => this.displayNotification({ message: _t("Upload cancelled") });
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {integer} loaded
|
||||
* @param {integer} total
|
||||
*/
|
||||
update(loaded, total) {
|
||||
if (!this.$el) {
|
||||
return;
|
||||
}
|
||||
const percent = Math.round((loaded / total) * 100);
|
||||
this.$('.o_file_upload_progress_bar_value').css("width", percent + "%");
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickCross(ev) {
|
||||
ev.stopPropagation();
|
||||
const promptText = _.str.sprintf(_t("Do you really want to cancel the upload of %s?"), _.escape(this.title));
|
||||
Dialog.confirm(this, promptText, {
|
||||
confirm_callback: () => {
|
||||
this.xhr.abort();
|
||||
this.trigger_up('progress_bar_abort', { fileUploadId: this.fileUploadId });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default ProgressBar;
|
||||
@@ -1,49 +0,0 @@
|
||||
/** @odoo-module alias=web.ProgressCard **/
|
||||
|
||||
import { _t } from 'web.core';
|
||||
import Widget from 'web.Widget';
|
||||
|
||||
const ProgressCard = Widget.extend({
|
||||
template: 'web.ProgressCard',
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} param1
|
||||
* @param {String} param1.title
|
||||
* @param {String} param1.type file mimetype
|
||||
* @param {String} param1.viewType
|
||||
*/
|
||||
init(parent, { title, type, viewType }) {
|
||||
this._super(...arguments);
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.viewType = viewType;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {integer} loaded
|
||||
* @param {integer} total
|
||||
*/
|
||||
update(loaded, total) {
|
||||
if (!this.$el) {
|
||||
return;
|
||||
}
|
||||
const percent = Math.round((loaded / total) * 100);
|
||||
const $textDivLeft = this.$('.o_file_upload_progress_text_left');
|
||||
const $textDivRight = this.$('.o_file_upload_progress_text_right');
|
||||
if (percent === 100) {
|
||||
$textDivLeft.text(_t('Processing...'));
|
||||
} else {
|
||||
const mbLoaded = Math.round(loaded/1000000);
|
||||
const mbTotal = Math.round(total/1000000);
|
||||
$textDivLeft.text(_.str.sprintf(_t("Uploading... (%s%%)"), percent));
|
||||
$textDivRight.text(_.str.sprintf(_t("(%s/%sMb)"), mbLoaded, mbTotal));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default ProgressCard;
|
||||
@@ -1,58 +0,0 @@
|
||||
.o_content {
|
||||
|
||||
.o_kanban_progress_card {
|
||||
|
||||
position: relative;
|
||||
padding: 0;
|
||||
min-height: 80px;
|
||||
cursor: pointer;
|
||||
|
||||
.o_kanban_record_bottom {
|
||||
color: $gray-900;
|
||||
}
|
||||
.o_kanban_image_wrapper {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.o_list_progress_card {
|
||||
height: 25px;
|
||||
border: 1px solid #dfdfdf;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
|
||||
.o_file_upload_upload_title {
|
||||
color: $headings-color;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.o_file_upload_progress_bar {
|
||||
@include o-position-absolute($top: 0px, $left: 0px);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.o_file_upload_progress_bar_value {
|
||||
transition: width 0.1s;
|
||||
border-right: 1px solid darken($o-brand-primary, 7.5%);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: $o-brand-primary;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.o_upload_cross {
|
||||
@include o-position-absolute(0, 0);
|
||||
padding: 4px;
|
||||
color: #963535;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
&:not(:hover) .o_upload_cross {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<div t-name="web.FileUploadProgressBar" class="o_file_upload_progress_bar">
|
||||
<div class="o_file_upload_progress_bar_value"/>
|
||||
<span class="o_upload_cross fa fa-times-circle" title="Cancel Upload" aria-label="Cancel Upload"/>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="web.ProgressCard">
|
||||
<t t-if="widget.viewType === 'kanban'">
|
||||
<div class="o_kanban_progress_card o_kanban_record o_kanban_attachment">
|
||||
<div class="o_kanban_image">
|
||||
<div class="o_kanban_image_wrapper">
|
||||
<div class="o_image o_image_thumbnail" t-att-data-mimetype="widget.type"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_details">
|
||||
<div class="o_kanban_details_wrapper">
|
||||
<div t-att-title="widget.title" t-att-aria-label="widget.title" class="o_kanban_record_title">
|
||||
<span t-esc="widget.title"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_body"/>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<div class="o_file_upload_progress_text_left">Processing...</div>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span class="o_file_upload_progress_text_right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="widget.viewType === 'list'">
|
||||
<div class="o_data_row o_list_progress_card">
|
||||
<span class="o_file_upload_upload_title" t-esc="widget.title"/>
|
||||
<span class="o_file_upload_progress_text_left">Processing...</span>
|
||||
<span class="o_file_upload_progress_text_right"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -26,7 +26,7 @@ const { COLORS } = ColorList;
|
||||
const formatters = registry.category("formatters");
|
||||
|
||||
// These classes determine whether a click on a record should open it.
|
||||
const CANCEL_GLOBAL_CLICK = ["a", ".dropdown", ".oe_kanban_action"].join(",");
|
||||
export const CANCEL_GLOBAL_CLICK = ["a", ".dropdown", ".oe_kanban_action"].join(",");
|
||||
const ALLOW_GLOBAL_CLICK = [".oe_kanban_global_click", ".oe_kanban_global_click_edit"].join(",");
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { FileUploadProgressContainer } from "@web/core/file_upload/file_upload_progress_container";
|
||||
import { FileUploadProgressRecord } from "@web/core/file_upload/file_upload_progress_record";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick, patchWithCleanup } from "../helpers/utils";
|
||||
|
||||
const { Component, xml } = owl;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
class FileUploadProgressTestRecord extends FileUploadProgressRecord {}
|
||||
FileUploadProgressTestRecord.template = xml`
|
||||
<t t-set="progressTexts" t-value="getProgressTexts()"/>
|
||||
<div class="file_upload">
|
||||
<div class="file_upload_progress_text_left" t-esc="progressTexts.left"/>
|
||||
<div class="file_upload_progress_text_right" t-esc="progressTexts.right"/>
|
||||
<FileUploadProgressBar fileUpload="props.fileUpload"/>
|
||||
</div>
|
||||
`;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.fileUploadService = useService("file_upload");
|
||||
this.FileUploadProgressTestRecord = FileUploadProgressTestRecord;
|
||||
}
|
||||
}
|
||||
Parent.components = {
|
||||
FileUploadProgressContainer,
|
||||
};
|
||||
Parent.template = xml`
|
||||
<div class="parent">
|
||||
<FileUploadProgressContainer fileUploads="fileUploadService.uploads" shouldDisplay="props.shouldDisplay" Component="FileUploadProgressTestRecord"/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let env;
|
||||
let target;
|
||||
let patchUpload;
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(async () => {
|
||||
serviceRegistry.add("file_upload", fileUploadService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
patchUpload = (customSend) => {
|
||||
patchWithCleanup(fileUploadService, {
|
||||
createXhr() {
|
||||
const xhr = {
|
||||
upload: new window.EventTarget(),
|
||||
open() {},
|
||||
send(data) {
|
||||
customSend && customSend(data);
|
||||
},
|
||||
};
|
||||
return xhr;
|
||||
},
|
||||
});
|
||||
};
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("FileUploadProgressContainer");
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".parent");
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload renders new component(s)", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
assert.containsN(target, ".file_upload", 2);
|
||||
});
|
||||
|
||||
QUnit.test("upload end removes component", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(new Event("load"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload error removes component", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(new Event("error"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload abort removes component", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(new Event("abort"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload can be aborted by clicking on cross", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
patchWithCleanup(env.services.dialog, {
|
||||
add: () => {
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(new Event("abort"));
|
||||
},
|
||||
});
|
||||
await click(target, ".o-file-upload-progress-bar-abort", true);
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload updates on progress", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
|
||||
const progressEvent = new Event("progress", { bubbles: true });
|
||||
progressEvent.loaded = 250000000;
|
||||
progressEvent.total = 500000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_left").textContent, "Uploading... (50%)");
|
||||
progressEvent.loaded = 350000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_right").textContent, "(350/500MB)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user