Initial commit: OCA Technical packages (595 packages)
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -0,0 +1,463 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>DMS Field</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="dms-field">
|
||||
<h1 class="title">DMS Field</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:4c2029fa91a7142bb6adb4fa9c78281736dbf3f2b2bce45b90ae068dba046f41
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/dms/tree/16.0/dms_field"><img alt="OCA/dms" src="https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/dms-16-0/dms-16-0-dms_field"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/dms&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This addon creates a new kind of view and allows to define a folder
|
||||
related to a record.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-3">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>To use the embedded view in any module, the module must inherit from the mixin
|
||||
dms.field.mixin (You have an example with res.partner in this module).</p>
|
||||
<p>Once this is done, in the form view of the model we will have to add the following:</p>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"dms_directory_ids"</span><span class="w"> </span><span class="na">mode=</span><span class="s">"dms_list"</span><span class="w"> </span><span class="nt">/></span>
|
||||
</pre>
|
||||
<p>In addition, it will be necessary to create an Embedded DMS template for this model.</p>
|
||||
<ol class="arabic simple">
|
||||
<li><em>Go to Documents > Configuration > Embedded DMS templates</em> and create a new record.</li>
|
||||
<li>Set a storage, a model (res.partner for example) and the access groups you want.</li>
|
||||
<li>You can also use expressions in “Directory format name”, for example: {{object.name}}</li>
|
||||
<li>Click on the “Documents” tab icon and a folder hierarchy will be created.</li>
|
||||
<li>You can set here the hierarchy of directories, subdirectories and files you need, this hierarchy will be used as a base when creating a new record (res.partner for example).</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>#. Go to the form view of an existing partner and click on the “DMS” tab icon, a hierarchy of
|
||||
folders and files linked to that record will be created.
|
||||
#. Create a new partner. A hierarchy of folders and files linked to that record will be created.</p>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Known issues / Roadmap</a></h1>
|
||||
<ul class="simple">
|
||||
<li>Add drag & drop compatibility to the dms_tree mode</li>
|
||||
<li>Multiple selection support (e.g. cut several files and paste to another folder).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/dms/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/dms/issues/new?body=module:%20dms_field%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Creu Blanca</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></li>
|
||||
<li>Jaime Arroyo <<a class="reference external" href="mailto:jaime.arroyo@creublanca.es">jaime.arroyo@creublanca.es</a>></li>
|
||||
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
|
||||
<li>Víctor Martínez</li>
|
||||
<li>Carlos Roca</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/dms/tree/16.0/dms_field">OCA/dms</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2014 Ivan Bozhanov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2014 Orange Hill Development
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
/** @odoo-module **/
|
||||
import utils from "web.field_utils";
|
||||
|
||||
export function formatBinarySize(value, field, options) {
|
||||
var new_options = _.defaults(options || {}, {
|
||||
si: true,
|
||||
});
|
||||
var thresh = new_options.si ? 1000 : 1024;
|
||||
if (Math.abs(value) < thresh) {
|
||||
return utils.format.float(value, field, options) + " B";
|
||||
}
|
||||
var units = new_options.si
|
||||
? ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
||||
var unit = -1;
|
||||
var new_value = value;
|
||||
do {
|
||||
new_value /= thresh;
|
||||
++unit;
|
||||
} while (Math.abs(new_value) >= thresh && unit < units.length - 1);
|
||||
return utils.format.float(new_value, field, new_options) + " " + units[unit];
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
var mapping = [
|
||||
["file-image-o", /^image\//],
|
||||
["file-audio-o", /^audio\//],
|
||||
["file-video-o", /^video\//],
|
||||
["file-pdf-o", "application/pdf"],
|
||||
["file-text-o", "text/plain"],
|
||||
["file-code-o", ["text/html", "text/javascript", "application/javascript"]],
|
||||
[
|
||||
"file-archive-o",
|
||||
[
|
||||
/^application\/x-(g?tar|xz|compress|bzip2|g?zip)$/,
|
||||
/^application\/x-(7z|rar|zip)-compressed$/,
|
||||
/^application\/(zip|gzip|tar)$/,
|
||||
],
|
||||
],
|
||||
[
|
||||
"file-word-o",
|
||||
[
|
||||
/ms-?word/,
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
],
|
||||
],
|
||||
[
|
||||
"file-powerpoint-o",
|
||||
[
|
||||
/ms-?powerpoint/,
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
],
|
||||
],
|
||||
[
|
||||
"file-excel-o",
|
||||
[
|
||||
/ms-?excel/,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
],
|
||||
],
|
||||
["file-o"],
|
||||
];
|
||||
|
||||
function match(mimetype, cond) {
|
||||
if (Array.isArray(cond)) {
|
||||
return cond.reduce(function (v, c) {
|
||||
return v || match(mimetype, c);
|
||||
}, false);
|
||||
} else if (cond instanceof RegExp) {
|
||||
return cond.test(mimetype);
|
||||
} else if (cond === undefined) {
|
||||
return true;
|
||||
}
|
||||
return mimetype === cond;
|
||||
}
|
||||
|
||||
var cache = {};
|
||||
function resolve(mimetype) {
|
||||
if (cache[mimetype]) {
|
||||
return cache[mimetype];
|
||||
}
|
||||
for (var i = 0; i < mapping.length; i++) {
|
||||
if (match(mimetype, mapping[i][1])) {
|
||||
cache[mimetype] = mapping[i][0];
|
||||
return mapping[i][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mimetype2fa(mimetype, options) {
|
||||
if (typeof mimetype === "object") {
|
||||
var new_options = mimetype;
|
||||
return function (new_mimetype) {
|
||||
return mimetype2fa(new_mimetype, new_options);
|
||||
};
|
||||
}
|
||||
var icon = resolve(mimetype);
|
||||
if (icon && options && options.prefix) {
|
||||
return options.prefix + icon;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// This code is necessary to avoid an incompatibility with the definition
|
||||
// .o_field_widget input, .o_field_widget textarea {color: inherit} added by
|
||||
// web_responsive. This causes the text to appear white when editing the name of
|
||||
// the selected file/directory on field mode (Example: field added on hr.employee by
|
||||
// hr_dms_field), making it impossible to see the content. It is necessary to maintain
|
||||
// it in version 16.0, and if migrating to higher versions, check if it remains
|
||||
// necessary.
|
||||
.jstree-proton .jstree-clicked {
|
||||
color: #000000 !important;
|
||||
i {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.jstree-proton a.jstree-clicked {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/** @odoo-module */
|
||||
/* Copyright 2024 Tecnativa - Carlos Roca
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
import {addFieldDependencies} from "@web/views/utils";
|
||||
import {Field} from "@web/views/fields/field";
|
||||
import {XMLParser} from "@web/core/utils/xml";
|
||||
|
||||
export class DmsListArchParser extends XMLParser {
|
||||
parseFieldNode(node, models, modelName) {
|
||||
return Field.parseFieldNode(node, models, modelName, "dms_list");
|
||||
}
|
||||
|
||||
parse(arch, models, modelName) {
|
||||
const fieldNodes = {};
|
||||
const activeFields = {};
|
||||
this.visitXML(arch, (node) => {
|
||||
if (node.tagName === "field") {
|
||||
const fieldInfo = this.parseFieldNode(node, models, modelName);
|
||||
fieldNodes[fieldInfo.name] = fieldInfo;
|
||||
node.setAttribute("field_id", fieldInfo.name);
|
||||
addFieldDependencies(
|
||||
activeFields,
|
||||
models[modelName],
|
||||
fieldInfo.FieldComponent.fieldDependencies
|
||||
);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
for (const [key, field] of Object.entries(fieldNodes)) {
|
||||
activeFields[key] = field; // TODO process
|
||||
}
|
||||
return {
|
||||
activeFields,
|
||||
__rawArch: arch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,538 @@
|
|||
/** @odoo-module */
|
||||
/* Copyright 2024 Tecnativa - Carlos Roca
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
|
||||
import {Layout} from "@web/search/layout";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {useModel} from "@web/views/model";
|
||||
const {Component, onRendered} = owl;
|
||||
import {session} from "@web/session";
|
||||
import {Deferred} from "@web/core/utils/concurrency";
|
||||
import {Domain} from "@web/core/domain";
|
||||
import {mimetype2fa} from "../../utils/mimetype.esm";
|
||||
import {formatBinarySize} from "../../utils/format_binary_size.esm";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {DynamicRecordList} from "@web/views/relational_model";
|
||||
|
||||
export const DMSListControllerObject = {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
this.http = useService("http");
|
||||
const {rootState} = this.props.state || {};
|
||||
this.model =
|
||||
(this.props.record && this.props.record.model) ||
|
||||
useModel(this.props.Model, {
|
||||
resModel: this.props.resModel,
|
||||
fields: this.props.fields,
|
||||
activeFields: this.props.archInfo.activeFields,
|
||||
viewMode: "dms_list",
|
||||
rootState,
|
||||
});
|
||||
this.resModel = this.props.resModel || this.props.record.resModel;
|
||||
this.rendererActions = {
|
||||
onDMSCreateEmptyStorages: this.onDMSCreateEmptyStorages.bind(this),
|
||||
onDMSLoad: this.onDMSLoad.bind(this),
|
||||
onDMSRenameNode: this.onDMSRenameNode.bind(this),
|
||||
onDMSMoveNode: this.onDMSMoveNode.bind(this),
|
||||
onDMSDeleteNode: this.onDMSDeleteNode.bind(this),
|
||||
onDMSDroppedFile: this.onDMSDroppedFile.bind(this),
|
||||
};
|
||||
onRendered(() => {
|
||||
this.processProps();
|
||||
});
|
||||
},
|
||||
sanitizeDMSModel(model) {
|
||||
return model;
|
||||
},
|
||||
processProps() {
|
||||
const model = this.sanitizeDMSModel(this.resModel);
|
||||
var storage_domain = [];
|
||||
var directory_domain = [];
|
||||
var autocompute_directory = false;
|
||||
var show_storage = true;
|
||||
if (model === "dms.storage") {
|
||||
if (this.model.root.data && this.model.root.data.id) {
|
||||
storage_domain = [["id", "=", this.model.root.data.id]];
|
||||
} else {
|
||||
storage_domain = [
|
||||
[
|
||||
"id",
|
||||
"in",
|
||||
this.model.root.records.map((record) => {
|
||||
return record.resId;
|
||||
}),
|
||||
],
|
||||
];
|
||||
}
|
||||
directory_domain = [];
|
||||
} else if (model === "dms.field.template") {
|
||||
if (this.model.root.resId) {
|
||||
storage_domain = [["id", "=", this.model.root.data.storage_id[0]]];
|
||||
} else {
|
||||
storage_domain = [["id", "=", 0]];
|
||||
}
|
||||
directory_domain = [
|
||||
[
|
||||
"root_directory_id",
|
||||
"in",
|
||||
this.model.root.data.dms_directory_ids.records.map((record) => {
|
||||
return record.resId;
|
||||
}),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
storage_domain = [["field_template_ids.model", "=", model]];
|
||||
autocompute_directory = true;
|
||||
show_storage = false;
|
||||
}
|
||||
this.params = {
|
||||
storage: {
|
||||
domain: storage_domain,
|
||||
context: session.user_context,
|
||||
show: show_storage,
|
||||
},
|
||||
directory: {
|
||||
domain: directory_domain,
|
||||
context: session.user_context,
|
||||
autocompute_directory: autocompute_directory,
|
||||
},
|
||||
file: {
|
||||
domain: [],
|
||||
context: session.user_context,
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
async onDMSLoad(node) {
|
||||
await this.model.root.load();
|
||||
this.model.notify();
|
||||
this.processProps();
|
||||
var args = this.buildDMSArgs();
|
||||
var result = false;
|
||||
if (!node || node.id === "#") {
|
||||
result = this.loadInitialData(args);
|
||||
} else {
|
||||
result = this.loadNode(node, args);
|
||||
}
|
||||
return {result, empty_storages: this.empty_storages};
|
||||
},
|
||||
loadInitialData(args) {
|
||||
var self = this;
|
||||
var data_loaded = new Deferred();
|
||||
this.empty_storages = [];
|
||||
this.loadStorages(args).then(
|
||||
function (storages) {
|
||||
var loading_data_parts = [];
|
||||
_.each(
|
||||
storages,
|
||||
function (storage, index) {
|
||||
if (storage.count_storage_directories > 0) {
|
||||
var directory_loaded = new Deferred();
|
||||
loading_data_parts.push(directory_loaded);
|
||||
this.loadDirectoriesSingle(storage.id, args).then(function (
|
||||
directories
|
||||
) {
|
||||
if (directories.length > 0) {
|
||||
storages[index].directories = directories;
|
||||
} else if (
|
||||
self.props.resModel !== "dms.directory" &&
|
||||
self.props.resModel !== "dms.storage"
|
||||
) {
|
||||
self.empty_storages.push(storage);
|
||||
}
|
||||
directory_loaded.resolve();
|
||||
});
|
||||
} else if (
|
||||
self.props.resModel !== "dms.directory" &&
|
||||
self.props.resModel !== "dms.storage"
|
||||
) {
|
||||
self.empty_storages.push(storage);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
$.when.apply($, loading_data_parts).then(
|
||||
function () {
|
||||
if (args.storage.show) {
|
||||
var result = _.chain(storages)
|
||||
.map(
|
||||
function (storage) {
|
||||
if (!storage.directories) {
|
||||
return undefined;
|
||||
}
|
||||
var children = _.map(
|
||||
storage.directories || [],
|
||||
function (directory) {
|
||||
return this.makeNodeDirectory(
|
||||
directory,
|
||||
args.file.show
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
return this.makeNodeStorage(storage, children);
|
||||
}.bind(this)
|
||||
)
|
||||
.filter(function (node) {
|
||||
return node;
|
||||
})
|
||||
.value();
|
||||
data_loaded.resolve(result);
|
||||
} else {
|
||||
var nodes = [];
|
||||
_.each(
|
||||
storages,
|
||||
function (storage) {
|
||||
_.each(
|
||||
storage.directories,
|
||||
function (directory) {
|
||||
nodes.push(
|
||||
this.makeNodeDirectory(
|
||||
directory,
|
||||
args.file.show,
|
||||
storage
|
||||
)
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
data_loaded.resolve(nodes);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
// Launch _update_overlay to show the drag and drop
|
||||
// this._update_overlay();
|
||||
}.bind(this)
|
||||
);
|
||||
return data_loaded;
|
||||
},
|
||||
loadNode(node, args) {
|
||||
var result = new Deferred();
|
||||
if (node.data && node.data.resModel === "dms.storage") {
|
||||
this.loadDirectoriesSingle(node.data.data.id, args).then(
|
||||
function (directories) {
|
||||
var directory_nodes = _.map(
|
||||
directories,
|
||||
function (directory) {
|
||||
return this.makeNodeDirectory(directory, args.file.show);
|
||||
}.bind(this)
|
||||
);
|
||||
result.resolve(directory_nodes);
|
||||
}.bind(this)
|
||||
);
|
||||
} else if (node.data && node.data.resModel === "dms.directory") {
|
||||
var files_loaded = new Deferred();
|
||||
var directories_loaded = new Deferred();
|
||||
this.loadSubdirectoriesSingle(node.data.data.id, args).then(
|
||||
function (directories) {
|
||||
var directory_nodes = _.map(
|
||||
directories,
|
||||
function (directory) {
|
||||
return this.makeNodeDirectory(directory, args.file.show);
|
||||
}.bind(this)
|
||||
);
|
||||
directories_loaded.resolve(directory_nodes);
|
||||
}.bind(this)
|
||||
);
|
||||
if (args.file.show) {
|
||||
this.loadFilesSingle(node.data.data.id, args).then(
|
||||
function (files) {
|
||||
var file_nodes = _.map(
|
||||
files,
|
||||
function (file) {
|
||||
return this.makeNodeFile(file);
|
||||
}.bind(this)
|
||||
);
|
||||
files_loaded.resolve(file_nodes);
|
||||
}.bind(this)
|
||||
);
|
||||
} else {
|
||||
files_loaded.resolve([]);
|
||||
}
|
||||
$.when(directories_loaded, files_loaded).then(function (
|
||||
directories,
|
||||
files
|
||||
) {
|
||||
result.resolve(directories.concat(files));
|
||||
});
|
||||
} else {
|
||||
result.resolve([]);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
makeNodeDirectory(directory, showFiles, storage) {
|
||||
var data = _.extend(directory, {
|
||||
name: directory.name,
|
||||
perm_read: directory.permission_read,
|
||||
perm_create: directory.permission_create,
|
||||
perm_write: directory.permission_write,
|
||||
perm_unlink: directory.permission_unlink,
|
||||
icon_url: directory.icon_url,
|
||||
count_total_directories: directory.count_total_directories,
|
||||
count_total_files: directory.count_total_files,
|
||||
human_size: directory.human_size,
|
||||
count_elements: directory.count_elements,
|
||||
});
|
||||
if (
|
||||
storage &&
|
||||
this.resModel !== "dms.directory" &&
|
||||
this.resModel !== "dms.storage"
|
||||
) {
|
||||
// We are assuming this is a record directory, so disabling actions
|
||||
data.name = storage.name;
|
||||
data.storage = true;
|
||||
}
|
||||
var dt = this.makeDataPoint({
|
||||
data: data,
|
||||
resModel: "dms.directory",
|
||||
});
|
||||
dt.parent = directory.parent_id ? "directory_" + directory.parent_id[0] : "#";
|
||||
var directoryNode = {
|
||||
id: dt.id,
|
||||
text: directory.name,
|
||||
icon: "fa fa-folder-o",
|
||||
type: "directory",
|
||||
data: dt,
|
||||
};
|
||||
if (showFiles) {
|
||||
directoryNode.children =
|
||||
directory.count_directories + directory.count_files > 0;
|
||||
} else {
|
||||
directoryNode.children = directory.count_directories > 0;
|
||||
}
|
||||
return directoryNode;
|
||||
},
|
||||
makeNodeFile(file) {
|
||||
var data = _.extend(file, {
|
||||
filename: file.name,
|
||||
display_name: file.name,
|
||||
binary_size: formatBinarySize(file.size),
|
||||
perm_read: file.permission_read,
|
||||
perm_create:
|
||||
file.permission_create && (!file.is_locked || file.is_lock_editor),
|
||||
perm_write:
|
||||
file.permission_write && (!file.is_locked || file.is_lock_editor),
|
||||
perm_unlink:
|
||||
file.permission_unlink && (!file.is_locked || file.is_lock_editor),
|
||||
icon_url: file.icon_url,
|
||||
});
|
||||
var dt = this.makeDataPoint({
|
||||
data: data,
|
||||
resModel: "dms.file",
|
||||
});
|
||||
return {
|
||||
id: dt.id,
|
||||
text: dt.data.display_name,
|
||||
icon: mimetype2fa(dt.data.mimetype, {prefix: "fa fa-"}),
|
||||
type: "file",
|
||||
data: dt,
|
||||
};
|
||||
},
|
||||
makeNodeStorage(storage, children) {
|
||||
var dt = this.makeDataPoint({
|
||||
data: storage,
|
||||
resModel: "dms.storage",
|
||||
});
|
||||
return {
|
||||
id: "storage_" + storage.id,
|
||||
text: storage.name,
|
||||
icon: "fa fa-database",
|
||||
type: "storage",
|
||||
data: dt,
|
||||
children: children,
|
||||
};
|
||||
},
|
||||
makeDataPoint(dt) {
|
||||
return new DynamicRecordList(this.model, dt);
|
||||
},
|
||||
loadDirectories(operator, value, args) {
|
||||
return this.orm.call("dms.directory", "search_read_parents", [], {
|
||||
fields: _.union(args.directory.fields || [], [
|
||||
"permission_read",
|
||||
"permission_create",
|
||||
"permission_write",
|
||||
"permission_unlink",
|
||||
"count_directories",
|
||||
"count_files",
|
||||
"name",
|
||||
"parent_id",
|
||||
"icon_url",
|
||||
"count_total_directories",
|
||||
"count_total_files",
|
||||
"human_size",
|
||||
"count_elements",
|
||||
"__last_update",
|
||||
]),
|
||||
domain: this.buildDMSDomain(
|
||||
[["storage_id", operator, value]],
|
||||
args.directory.domain,
|
||||
args.directory.autocompute_directory
|
||||
),
|
||||
context: args.directory.context || session.user_context,
|
||||
});
|
||||
},
|
||||
loadDirectoriesSingle(storage_id, args) {
|
||||
return this.loadDirectories("=", storage_id, args);
|
||||
},
|
||||
loadSubdirectories(operator, value, args) {
|
||||
const domain = this.buildDMSDomain(
|
||||
[["parent_id", operator, value]],
|
||||
args.directory.domain,
|
||||
false
|
||||
);
|
||||
const fields = _.union(args.directory.fields || [], [
|
||||
"permission_read",
|
||||
"permission_create",
|
||||
"permission_write",
|
||||
"permission_unlink",
|
||||
"count_directories",
|
||||
"count_files",
|
||||
"name",
|
||||
"parent_id",
|
||||
"icon_url",
|
||||
"count_total_directories",
|
||||
"count_total_files",
|
||||
"human_size",
|
||||
"count_elements",
|
||||
"__last_update",
|
||||
]);
|
||||
return this.orm.searchRead("dms.directory", domain, fields, {
|
||||
context: args.file.context || session.user_context,
|
||||
});
|
||||
},
|
||||
loadSubdirectoriesSingle(directory_id, args) {
|
||||
return this.loadSubdirectories("=", directory_id, args);
|
||||
},
|
||||
loadFiles(operator, value, args) {
|
||||
const domain = this.buildDMSDomain(
|
||||
[["directory_id", operator, value]],
|
||||
args.file.domain
|
||||
);
|
||||
const fields = _.union(args.file.fields || [], [
|
||||
"permission_read",
|
||||
"permission_create",
|
||||
"permission_write",
|
||||
"permission_unlink",
|
||||
"icon_url",
|
||||
"name",
|
||||
"mimetype",
|
||||
"directory_id",
|
||||
"human_size",
|
||||
"is_locked",
|
||||
"is_lock_editor",
|
||||
"extension",
|
||||
"__last_update",
|
||||
]);
|
||||
return this.orm.searchRead("dms.file", domain, fields, {
|
||||
context: args.file.context || session.user_context,
|
||||
});
|
||||
},
|
||||
loadFilesSingle(directory_id, args) {
|
||||
return this.loadFiles("=", directory_id, args);
|
||||
},
|
||||
loadStorages(args) {
|
||||
const fields = _.union(args.storage.fields || [], [
|
||||
"name",
|
||||
"count_storage_directories",
|
||||
]);
|
||||
return this.orm.searchRead("dms.storage", args.storage.domain || [], fields, {
|
||||
context: args.storage.context || session.user_context,
|
||||
});
|
||||
},
|
||||
buildDMSDomain(base, domain, autocompute_directory) {
|
||||
var result = new Domain(base);
|
||||
if (autocompute_directory) {
|
||||
result = Domain.and([
|
||||
result,
|
||||
new Domain([["res_id", "=", this.model.root.resId]]),
|
||||
]);
|
||||
} else {
|
||||
result = Domain.and([result, new Domain(domain || [])]);
|
||||
}
|
||||
return result.toList();
|
||||
},
|
||||
buildDMSArgs() {
|
||||
return {
|
||||
...this.params,
|
||||
search: {
|
||||
operator: "ilike",
|
||||
},
|
||||
};
|
||||
},
|
||||
onDMSCreateEmptyStorages() {
|
||||
var data = {
|
||||
model: this.sanitizeDMSModel(this.resModel),
|
||||
empty_storages: this.empty_storages,
|
||||
res_id: this.props.record.resId,
|
||||
};
|
||||
return this.orm.call("dms.field.template", "create_dms_directory", [], {
|
||||
context: {
|
||||
res_id: data.res_id,
|
||||
res_model: data.model,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDMSRenameNode(node, text) {
|
||||
node.data.data.name = text;
|
||||
return this.orm.write(node.data.resModel, [node.data.data.id], {
|
||||
name: text,
|
||||
});
|
||||
},
|
||||
onDMSMoveNode(node, newParent) {
|
||||
var data = {};
|
||||
if (node.data.resModel === "dms.file") {
|
||||
data.directory_id = newParent.data.data.id;
|
||||
} else if (node.data.resModel === "dms.directory") {
|
||||
data.parent_id = newParent.data.data.id;
|
||||
}
|
||||
return this.orm.write(node.data.resModel, [node.data.data.id], data);
|
||||
},
|
||||
onDMSDeleteNode(node) {
|
||||
return this.orm.unlink(node.data.resModel, [node.data.data.id]);
|
||||
},
|
||||
async onDMSDroppedFile(directoryId, files) {
|
||||
const params = {
|
||||
csrf_token: odoo.csrf_token,
|
||||
ufile: [...files],
|
||||
model: "dms.file",
|
||||
id: 0,
|
||||
};
|
||||
const fileData = await this.http.post(
|
||||
"/web/binary/upload_attachment",
|
||||
params,
|
||||
"text"
|
||||
);
|
||||
const attachments = JSON.parse(fileData);
|
||||
if (attachments.error) {
|
||||
throw new Error(attachments.error);
|
||||
}
|
||||
const attachmentIds = attachments.map((a) => a.id);
|
||||
const ctx = this.props.context || this.props.record.context;
|
||||
if (!attachmentIds.length) {
|
||||
return "no_attachments";
|
||||
}
|
||||
ctx.default_directory_id = directoryId;
|
||||
const attachment_datas = await this.orm.call(
|
||||
"dms.file",
|
||||
"get_dms_files_from_attachments",
|
||||
["", attachmentIds]
|
||||
);
|
||||
const attachments_args = [];
|
||||
attachment_datas.forEach((attachment_data) => {
|
||||
attachments_args.push({
|
||||
name: attachment_data.name,
|
||||
content: attachment_data.datas,
|
||||
mimetype: attachment_data.mimetype,
|
||||
});
|
||||
});
|
||||
return this.orm.call("dms.file", "create", [attachments_args], {
|
||||
context: ctx,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export class DmsListController extends Component {}
|
||||
patch(DmsListController.prototype, "DmsListControllerPatch", DMSListControllerObject);
|
||||
DmsListController.template = "dms_field.View";
|
||||
DmsListController.components = {Layout};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="dms_field.View" owl="1">
|
||||
<Layout display="props.display" className="'h-100 overflow-auto'">
|
||||
<t t-component="props.Renderer" rendererActions="rendererActions" />
|
||||
</Layout>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,532 @@
|
|||
/** @odoo-module */
|
||||
/* Copyright 2024 Tecnativa - Carlos Roca
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
|
||||
import {_lt} from "@web/core/l10n/translation";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {loadCSS, loadJS} from "@web/core/assets";
|
||||
const {Component, onMounted, onWillStart, useEffect, useRef, useState} = owl;
|
||||
import {download} from "@web/core/network/download";
|
||||
import {FormViewDialog} from "@web/views/view_dialogs/form_view_dialog";
|
||||
|
||||
export class DmsListRenderer extends Component {
|
||||
setup() {
|
||||
this.js_tree = useRef("jstree");
|
||||
this.extra_actions = useRef("extra_actions");
|
||||
this.dms_add_directory = useRef("dms_add_directory");
|
||||
this.nodeSelectedState = useState({data: {}});
|
||||
this.messaging = useService("messaging");
|
||||
this.notification = useService("notification");
|
||||
this.dialog = useService("dialog");
|
||||
this.dragState = useState({
|
||||
showDragZone: false,
|
||||
});
|
||||
this.dropZone = useRef("dropZone");
|
||||
|
||||
useEffect(
|
||||
(el) => {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
const highlight = this.highlight.bind(this);
|
||||
const unhighlight = this.unhighlight.bind(this);
|
||||
const drop = this.onDrop.bind(this);
|
||||
el.addEventListener("dragover", highlight);
|
||||
el.addEventListener("dragleave", unhighlight);
|
||||
el.addEventListener("drop", drop);
|
||||
return () => {
|
||||
el.removeEventListener("dragover", highlight);
|
||||
el.removeEventListener("dragleave", unhighlight);
|
||||
el.removeEventListener("drop", drop);
|
||||
};
|
||||
},
|
||||
|
||||
() => [this.dropZone.el]
|
||||
);
|
||||
onWillStart(async () => {
|
||||
await loadJS("/dms_field/static/lib/jsTree/jstree.js");
|
||||
await loadCSS("/dms_field/static/lib/jsTree/themes/proton/style.css");
|
||||
this.config = this.buildTreeConfig();
|
||||
});
|
||||
onMounted(() => {
|
||||
this.$tree = $(this.js_tree.el);
|
||||
this.$tree.jstree(this.config);
|
||||
this.startTreeTriggers();
|
||||
});
|
||||
}
|
||||
buildTreeConfig() {
|
||||
var plugins = [
|
||||
"conditionalselect",
|
||||
"massload",
|
||||
"wholerow",
|
||||
"state",
|
||||
"sort",
|
||||
"search",
|
||||
"types",
|
||||
"contextmenu",
|
||||
];
|
||||
return {
|
||||
core: {
|
||||
widget: this,
|
||||
animation: 0,
|
||||
multiple: false,
|
||||
check_callback: this.checkCallback.bind(this),
|
||||
themes: {
|
||||
name: "proton",
|
||||
responsive: true,
|
||||
},
|
||||
data: this.loadData.bind(this),
|
||||
},
|
||||
contextmenu: {
|
||||
items: this.loadContextMenu.bind(this),
|
||||
},
|
||||
state: {
|
||||
key: "documents",
|
||||
},
|
||||
conditionalselect: this.checkSelect.bind(this),
|
||||
plugins: plugins,
|
||||
sort: function (a, b) {
|
||||
// Correctly sort the records according to the type of element
|
||||
// (folder or file).
|
||||
// Do not use node.icon because they may have (or will have) a
|
||||
// different icon for each file according to its extension.
|
||||
var node_a = this.get_node(a);
|
||||
var node_b = this.get_node(b);
|
||||
if (node_a.data.resModel === node_b.data.resModel) {
|
||||
return node_a.text > node_b.text ? 1 : -1;
|
||||
}
|
||||
return node_a.data.resModel > node_b.data.resModel ? 1 : -1;
|
||||
},
|
||||
};
|
||||
}
|
||||
startTreeTriggers() {
|
||||
this.$tree.on("open_node.jstree", (e, data) => {
|
||||
if (data.node.data && data.node.data.resModel === "dms.directory") {
|
||||
data.instance.set_icon(data.node, "fa fa-folder-open-o");
|
||||
}
|
||||
});
|
||||
this.$tree.on("close_node.jstree", (e, data) => {
|
||||
if (data.node.data && data.node.data.resModel === "dms.directory") {
|
||||
data.instance.set_icon(data.node, "fa fa-folder-o");
|
||||
}
|
||||
});
|
||||
this.$tree.on("changed.jstree", (e, data) => {
|
||||
this.treeChanged(data);
|
||||
});
|
||||
this.$tree.on("move_node.jstree", (e, data) => {
|
||||
var jstree = this.$tree.jstree(true);
|
||||
this.props.rendererActions.onDMSMoveNode(
|
||||
data.node,
|
||||
jstree.get_node(data.parent)
|
||||
);
|
||||
});
|
||||
this.$tree.on("rename_node.jstree", (e, data) => {
|
||||
this.props.rendererActions.onDMSRenameNode(data.node, data.text);
|
||||
this.updatePreview(data.node);
|
||||
});
|
||||
this.$tree.on("delete_node.jstree", (e, data) => {
|
||||
this.props.rendererActions.onDMSDeleteNode(data.node);
|
||||
});
|
||||
this.$tree.on("loaded.jstree", () => {
|
||||
this.$tree.jstree("open_all");
|
||||
});
|
||||
}
|
||||
|
||||
treeChanged(data) {
|
||||
if (
|
||||
data.action === "select_node" &&
|
||||
data.selected &&
|
||||
data.selected.length === 1
|
||||
) {
|
||||
this.updatePreview(data.node);
|
||||
}
|
||||
}
|
||||
|
||||
updatePreview(node) {
|
||||
var $buttons = $(this.extra_actions.el);
|
||||
$buttons.empty();
|
||||
if (
|
||||
node.data &&
|
||||
["dms.directory", "dms.file"].indexOf(node.data.resModel) !== -1
|
||||
) {
|
||||
this.nodeSelectedState.data = {};
|
||||
this.nodeSelectedState.data = node.data;
|
||||
var menu = this.loadContextMenu(node);
|
||||
_.each(menu, (action) => {
|
||||
this.generateActionButton(node, action, $buttons);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadContextMenu(node) {
|
||||
var menu = {};
|
||||
var jstree = this.$tree.jstree(true);
|
||||
if (node.data) {
|
||||
if (node.data.resModel === "dms.directory") {
|
||||
menu = this.loadContextMenuDirectoryBefore(jstree, node, menu);
|
||||
menu = this.loadContextMenuBasic(jstree, node, menu);
|
||||
menu = this.loadContextMenuDirectory(jstree, node, menu);
|
||||
} else if (node.data.resModel === "dms.file") {
|
||||
menu = this.loadContextMenuBasic(jstree, node, menu);
|
||||
menu = this.loadContextMenuFile(jstree, node, menu);
|
||||
}
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
loadContextMenuBasic($jstree, node, menu) {
|
||||
menu.rename = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-pencil",
|
||||
label: _lt("Rename"),
|
||||
action: () => {
|
||||
$jstree.edit(node);
|
||||
},
|
||||
_disabled: () => {
|
||||
return !node.data.data.perm_write || node.data.data.storage;
|
||||
},
|
||||
};
|
||||
menu.action = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-bolt",
|
||||
label: _lt("Actions"),
|
||||
action: false,
|
||||
submenu: {
|
||||
cut: {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-scissors",
|
||||
label: _lt("Cut"),
|
||||
action: () => {
|
||||
$jstree.cut(node);
|
||||
},
|
||||
_disabled: () => {
|
||||
return !node.data.data.perm_read || node.data.data.storage;
|
||||
},
|
||||
},
|
||||
},
|
||||
_disabled: () => {
|
||||
return !node.data.data.perm_read;
|
||||
},
|
||||
};
|
||||
menu.delete = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-trash-o",
|
||||
label: _lt("Delete"),
|
||||
action: () => {
|
||||
$jstree.delete_node(node);
|
||||
},
|
||||
_disabled: () => {
|
||||
return !node.data.data.perm_unlink || node.data.data.storage;
|
||||
},
|
||||
};
|
||||
menu.open = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-external-link",
|
||||
label: _lt("Open"),
|
||||
action: () => {
|
||||
this.onDMSOpenRecord(node);
|
||||
},
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
loadContextMenuDirectoryBefore($jstree, node, menu) {
|
||||
menu.add_directory = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-folder",
|
||||
label: _lt("Create directory"),
|
||||
action: () => {
|
||||
this.onDMSAddDirectory(node);
|
||||
},
|
||||
_disabled: () => {
|
||||
return !node.data.data.perm_create;
|
||||
},
|
||||
};
|
||||
menu.add_file = {
|
||||
separator_before: false,
|
||||
separator_after: true,
|
||||
icon: "fa fa-file",
|
||||
label: _lt("Create File"),
|
||||
action: () => {
|
||||
this.onDMSAddFile(node);
|
||||
},
|
||||
_disabled: () => {
|
||||
return !node.data.data.perm_create;
|
||||
},
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
loadContextMenuDirectory($jstree, node, menu) {
|
||||
if (menu.action && menu.action.submenu) {
|
||||
menu.action.submenu.paste = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-clipboard",
|
||||
label: _lt("Paste"),
|
||||
action: () => {
|
||||
$jstree.paste(node);
|
||||
},
|
||||
_disabled: () => {
|
||||
return !$jstree.can_paste() && !node.data.data.perm_create;
|
||||
},
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
loadContextMenuFile($jstree, node, menu) {
|
||||
menu.preview = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-eye",
|
||||
label: _lt("Preview"),
|
||||
action: () => {
|
||||
this.onDMSPreviewFile(node);
|
||||
},
|
||||
};
|
||||
menu.download = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
icon: "fa fa-download",
|
||||
label: _lt("Download"),
|
||||
action: () => {
|
||||
download({
|
||||
url: "/web/content",
|
||||
data: {
|
||||
id: node.data.data.id,
|
||||
download: true,
|
||||
field: "content",
|
||||
model: "dms.file",
|
||||
filename_field: "name",
|
||||
filename: node.data.data.filename,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
generateActionButton(node, action, $buttons) {
|
||||
if (action.action) {
|
||||
var $button = $("<button>", {
|
||||
type: "button",
|
||||
class: "btn btn-secondary " + action.icon,
|
||||
"data-toggle": "dropdown",
|
||||
title: action.label,
|
||||
}).on("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (action._disabled && action._disabled()) {
|
||||
return;
|
||||
}
|
||||
action.action();
|
||||
});
|
||||
$buttons.append($button);
|
||||
}
|
||||
if (action.submenu) {
|
||||
_.each(action.submenu, (sub_action) => {
|
||||
this.generateActionButton(node, sub_action, $buttons);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadData(node, callback) {
|
||||
const {result, empty_storages} = await this.props.rendererActions.onDMSLoad(
|
||||
node
|
||||
);
|
||||
result.then((data) => {
|
||||
callback.call(this, data);
|
||||
if (empty_storages.length > 0) {
|
||||
$(this.dms_add_directory.el).removeClass("o_hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
This is used to check that the operation is allowed
|
||||
*/
|
||||
checkCallback(operation, node, parent) {
|
||||
if (operation === "copy_node" || operation === "move_node") {
|
||||
// Prevent moving a root node
|
||||
if (node.parent === "#") {
|
||||
return false;
|
||||
}
|
||||
// Prevent moving a child above or below the root
|
||||
if (parent.id === "#") {
|
||||
return false;
|
||||
}
|
||||
// Prevent moving a child to a settings object
|
||||
if (parent.data && parent.data.resModel === "dms.storage") {
|
||||
return false;
|
||||
}
|
||||
// Prevent moving a child to a file
|
||||
if (parent.data && parent.data.resModel === "dms.file") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
checkSelect(node) {
|
||||
if (this.props.filesOnly && node.data.resModel !== "dms.file") {
|
||||
return false;
|
||||
}
|
||||
return !(node.parent === "#" && node.data.resModel === "dms.storage");
|
||||
}
|
||||
onDMSAddDirectory(node) {
|
||||
var context = {
|
||||
default_parent_directory_id: node.data.data.id,
|
||||
};
|
||||
this.dialog.add(FormViewDialog, {
|
||||
resModel: "dms.directory",
|
||||
context: context,
|
||||
title: _lt("Add Directory: ") + node.data.data.name,
|
||||
onRecordSaved: () => {
|
||||
const selected_id = this.$tree.find(".jstree-clicked").attr("id");
|
||||
const model_data = this.$tree.jstree(true)._model.data;
|
||||
const state = this.$tree.jstree(true).get_state();
|
||||
const open_res_ids = state.core.open.map(
|
||||
(id) => model_data[id].data.data.id
|
||||
);
|
||||
this.$tree.on("refresh_node.jstree", () => {
|
||||
const model_data_entries = Object.entries(model_data);
|
||||
const ids = model_data_entries
|
||||
.filter(
|
||||
([, value]) =>
|
||||
value.data &&
|
||||
open_res_ids.includes(value.data.data.id) &&
|
||||
value.data.resModel === "dms.directory"
|
||||
)
|
||||
.map((tuple) => tuple[0]);
|
||||
for (var id of ids) {
|
||||
this.$tree.jstree(true).open_node(id);
|
||||
}
|
||||
});
|
||||
this.$tree.jstree(true).refresh_node(selected_id);
|
||||
},
|
||||
});
|
||||
}
|
||||
onDMSAddFile(node) {
|
||||
var context = {
|
||||
default_directory_id: node.data.data.id,
|
||||
};
|
||||
this.dialog.add(FormViewDialog, {
|
||||
resModel: "dms.file",
|
||||
context: context,
|
||||
title: _lt("Add File: ") + node.data.data.name,
|
||||
onRecordSaved: () => {
|
||||
const selected_id = this.$tree.find(".jstree-clicked").attr("id");
|
||||
const model_data = this.$tree.jstree(true)._model.data;
|
||||
const state = this.$tree.jstree(true).get_state();
|
||||
const open_res_ids = state.core.open.map(
|
||||
(id) => model_data[id].data.data.id
|
||||
);
|
||||
this.$tree.on("refresh_node.jstree", () => {
|
||||
const model_data_entries = Object.entries(model_data);
|
||||
const ids = model_data_entries
|
||||
.filter(
|
||||
([, value]) =>
|
||||
value.data &&
|
||||
open_res_ids.includes(value.data.data.id) &&
|
||||
value.data.model === "dms.directory"
|
||||
)
|
||||
.map((tuple) => tuple[0]);
|
||||
for (var id of ids) {
|
||||
this.$tree.jstree(true).open_node(id);
|
||||
}
|
||||
});
|
||||
this.$tree.jstree(true).refresh_node(selected_id);
|
||||
},
|
||||
});
|
||||
}
|
||||
onDMSAddDirectoryRecord() {
|
||||
this.props.rendererActions.onDMSCreateEmptyStorages().then(() => {
|
||||
this.$tree.jstree(true).refresh();
|
||||
$(this.dms_add_directory.el).addClass("o_hidden");
|
||||
});
|
||||
}
|
||||
onDMSOpenRecord(node) {
|
||||
this.dialog.add(FormViewDialog, {
|
||||
resModel: node.data.resModel,
|
||||
title: _lt("Open: ") + node.data.data.name,
|
||||
resId: node.data.data.id,
|
||||
});
|
||||
}
|
||||
onDMSPreviewFile(node) {
|
||||
this.messaging.get().then((messaging) => {
|
||||
const attachmentList = messaging.models.AttachmentList.insert({
|
||||
selectedAttachment: messaging.models.Attachment.insert({
|
||||
id: node.data.data.id,
|
||||
filename: node.data.data.name,
|
||||
name: node.data.data.name,
|
||||
mimetype: node.data.data.mimetype,
|
||||
model_name: node.data.resModel,
|
||||
}),
|
||||
});
|
||||
Component.env.services.dialog = messaging.models.Dialog.insert({
|
||||
attachmentListOwnerAsAttachmentView: attachmentList,
|
||||
});
|
||||
});
|
||||
}
|
||||
get showDragZone() {
|
||||
return (
|
||||
this.nodeSelectedState.data.resModel === "dms.directory" &&
|
||||
this.dragState.showDragZone
|
||||
);
|
||||
}
|
||||
highlight(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.dragState.showDragZone = true;
|
||||
}
|
||||
unhighlight(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.dragState.showDragZone = false;
|
||||
}
|
||||
async onDrop(ev) {
|
||||
ev.preventDefault();
|
||||
const directoryId = this.nodeSelectedState.data.data.id;
|
||||
const res = await this.props.rendererActions
|
||||
.onDMSDroppedFile(directoryId, ev.dataTransfer.files)
|
||||
.catch((error) => {
|
||||
this.notification.add(error.data.message, {
|
||||
type: "danger",
|
||||
});
|
||||
});
|
||||
if (res === "no_attachments") {
|
||||
this.notification.add(_lt("An error occurred during the upload"));
|
||||
} else {
|
||||
const selected_id = this.$tree.find(".jstree-clicked").attr("id");
|
||||
const model_data = this.$tree.jstree(true)._model.data;
|
||||
const state = this.$tree.jstree(true).get_state();
|
||||
const open_res_ids = state.core.open.map(
|
||||
(id) => model_data[id].data.data.id
|
||||
);
|
||||
this.$tree.on("refresh_node.jstree", () => {
|
||||
const model_data_entries = Object.entries(model_data);
|
||||
const ids = model_data_entries
|
||||
.filter(
|
||||
([, value]) =>
|
||||
value.data &&
|
||||
open_res_ids.includes(value.data.data.id) &&
|
||||
value.data.model === "dms.directory"
|
||||
)
|
||||
.map((tuple) => tuple[0]);
|
||||
for (var id of ids) {
|
||||
this.$tree.jstree(true).open_node(id);
|
||||
}
|
||||
});
|
||||
this.$tree.jstree(true).refresh_node(selected_id);
|
||||
}
|
||||
this.unhighlight(ev);
|
||||
}
|
||||
}
|
||||
|
||||
DmsListRenderer.template = "dms_list.Renderer";
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
.dms_document_controls {
|
||||
padding: 5px 0 10px 0;
|
||||
}
|
||||
|
||||
.dms_document_preview {
|
||||
height: 100% !important;
|
||||
.o_preview_directory_body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -16px;
|
||||
margin-left: -16px;
|
||||
.o_preview_directory_icon {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.o_preview_directory_table_body {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dms_document_col_preview {
|
||||
position: relative;
|
||||
.o_dropzone {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: #aaaa;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
top: 0;
|
||||
i {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dms_treeview {
|
||||
height: 100%;
|
||||
.dms_document_container {
|
||||
height: 100%;
|
||||
.dms_document_col {
|
||||
padding: 0;
|
||||
width: 50%;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
.dms_document_col {
|
||||
width: 100%;
|
||||
}
|
||||
.dms_document_col_preview {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.dms_document_col_tree > div {
|
||||
border-right-width: 3px;
|
||||
border-right-style: solid;
|
||||
border-right-color: #888;
|
||||
}
|
||||
}
|
||||
.dms_document_row {
|
||||
height: 100% !important;
|
||||
.dms_document_col_tree > div {
|
||||
height: 100%;
|
||||
.dms_document_tree {
|
||||
height: 100%;
|
||||
.dms_content,
|
||||
.dms_tree {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vakata-context {
|
||||
z-index: 9999;
|
||||
|
||||
.vakata-context-parent + ul {
|
||||
list-style: none;
|
||||
left: 100%;
|
||||
margin-top: -2.58em;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="dms_list.Renderer" owl="1">
|
||||
<div class="dms_treeview">
|
||||
<div class="container-fluid dms_document_container ">
|
||||
<div class="row o_dms_header btn-group">
|
||||
<button
|
||||
class="btn btn-secondary o_dms_add_directory fa fa-database o_hidden"
|
||||
t-ref="dms_add_directory"
|
||||
t-on-click="onDMSAddDirectoryRecord"
|
||||
/>
|
||||
<div class="o_dms_extra_actions btn-group" t-ref="extra_actions" />
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="row dms_document_row #{env.isSmall ? 'dms_document_mobile' : 'dms_document_desktop'}"
|
||||
>
|
||||
<div class="dms_document_col dms_document_col_tree">
|
||||
<div class="dms_document_tree">
|
||||
<div class="dms_content">
|
||||
<div class="dms_tree" t-ref="jstree" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dms_document_col dms_document_col_preview"
|
||||
t-ref="dropZone"
|
||||
>
|
||||
<div t-if="showDragZone" class="o_dropzone">
|
||||
<i class="fa fa-cloud-upload fa-10x" />
|
||||
</div>
|
||||
<div class="dms_document_preview">
|
||||
<t t-call="dms_list.DocumentTreeViewDirectoryPreview" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="dms_list.DocumentTreeViewDirectoryPreview" owl="1">
|
||||
<div
|
||||
class="o_preview_directory"
|
||||
t-if="Object.entries(nodeSelectedState.data).length !== 0"
|
||||
t-att-data-directory-id="nodeSelectedState.data.resModel === 'dms.directory' ? nodeSelectedState.data.res_id : ''"
|
||||
>
|
||||
<div class="top_info row">
|
||||
<div class="left_info col-4">
|
||||
<div class="o_preview_directory_icon" align="center">
|
||||
<div>
|
||||
<img
|
||||
t-if="nodeSelectedState.data.resModel === 'dms.directory'"
|
||||
class="h-100 w-100"
|
||||
t-att-src="nodeSelectedState.data.data.icon_url"
|
||||
/>
|
||||
<a
|
||||
t-if="nodeSelectedState.data.resModel === 'dms.file'"
|
||||
class="o_preview_file"
|
||||
t-att-data-id="nodeSelectedState.data.id"
|
||||
>
|
||||
<img
|
||||
class="h-100 w-100"
|
||||
t-att-src="nodeSelectedState.data.data.icon_url"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right_info col-8">
|
||||
<h3>
|
||||
<t t-esc="nodeSelectedState.data.data.name" />
|
||||
</h3>
|
||||
<t t-if="nodeSelectedState.data.resModel === 'dms.directory'">
|
||||
<p><b>Subdirectories:</b> <span
|
||||
t-esc="nodeSelectedState.data.data.count_total_directories"
|
||||
/></p>
|
||||
<p><b>Files:</b> <span
|
||||
t-esc="nodeSelectedState.data.data.count_total_files"
|
||||
/></p>
|
||||
<p><b>Size:</b> <span
|
||||
t-if="nodeSelectedState.data.data.human_size"
|
||||
t-esc="nodeSelectedState.data.data.human_size"
|
||||
/></p>
|
||||
<p><b>Elements:</b> <span
|
||||
t-esc="nodeSelectedState.data.data.count_elements"
|
||||
/></p>
|
||||
</t>
|
||||
<t t-if="nodeSelectedState.data.resModel === 'dms.file'">
|
||||
<p><b>Size:</b> <span
|
||||
t-esc="nodeSelectedState.data.data.human_size"
|
||||
/></p>
|
||||
</t>
|
||||
<div class="bottom_buttons">
|
||||
<a
|
||||
t-if="nodeSelectedState.data.resModel === 'dms.file'"
|
||||
class="btn btn-primary"
|
||||
t-attf-href="/web/content?id=#{nodeSelectedState.data.data.id}&field=content&model=dms.file&filename_field=name&download=true"
|
||||
>
|
||||
<i class="fa fa-download" />
|
||||
Download
|
||||
</a>
|
||||
<button
|
||||
class="btn btn-primary o_preview_file"
|
||||
t-if="nodeSelectedState.data.resModel === 'dms.file'"
|
||||
t-on-click="() => this.onDMSPreviewFile(nodeSelectedState)"
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import {registry} from "@web/core/registry";
|
||||
import {DmsListController} from "./dms_list_controller.esm";
|
||||
import {DmsListArchParser} from "./dms_list_arch_parser.esm";
|
||||
import {RelationalModel} from "@web/views/relational_model";
|
||||
import {DmsListRenderer} from "./dms_list_renderer.esm";
|
||||
|
||||
export const dmsListView = {
|
||||
type: "dms_list",
|
||||
display_name: "Dms Tree",
|
||||
icon: "fa fa-file-o",
|
||||
multiRecord: true,
|
||||
Controller: DmsListController,
|
||||
ArchParser: DmsListArchParser,
|
||||
Renderer: DmsListRenderer,
|
||||
Model: RelationalModel,
|
||||
|
||||
props(genericProps, view) {
|
||||
const {ArchParser} = view;
|
||||
const {arch, relatedModels, resModel} = genericProps;
|
||||
const archInfo = new ArchParser().parse(arch, relatedModels, resModel);
|
||||
|
||||
return {
|
||||
...genericProps,
|
||||
Model: view.Model,
|
||||
Renderer: view.Renderer,
|
||||
archInfo,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("views").add("dms_list", dmsListView);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module */
|
||||
/* Copyright 2024 Tecnativa - Carlos Roca
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
import {X2ManyField} from "@web/views/fields/x2many/x2many_field";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {DmsListRenderer} from "../../dms_list/dms_list_renderer.esm";
|
||||
import {DMSListControllerObject} from "../../dms_list/dms_list_controller.esm";
|
||||
|
||||
patch(X2ManyField.prototype, "dms_field.X2ManyField", {
|
||||
...DMSListControllerObject,
|
||||
get rendererProps() {
|
||||
const archInfo = this.activeField.views[this.viewMode];
|
||||
const props = {
|
||||
archInfo,
|
||||
list: this.list,
|
||||
openRecord: this.openRecord.bind(this),
|
||||
};
|
||||
if (this.viewMode === "dms_list") {
|
||||
props.archInfo = archInfo;
|
||||
props.readonly = this.props.readonly;
|
||||
props.rendererActions = this.rendererActions;
|
||||
return props;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
X2ManyField.components = {...X2ManyField.components, DmsListRenderer};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t
|
||||
t-name="dms_field.X2ManyField"
|
||||
t-inherit="web.X2ManyField"
|
||||
t-inherit-mode="extension"
|
||||
owl="1"
|
||||
>
|
||||
<xpath expr="//ListRenderer" position="after">
|
||||
<DmsListRenderer t-elif="viewMode == 'dms_list'" t-props="rendererProps" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||