Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -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&amp;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">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;dms_directory_ids&quot;</span><span class="w"> </span><span class="na">mode=</span><span class="s">&quot;dms_list&quot;</span><span class="w"> </span><span class="nt">/&gt;</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 &gt; Configuration &gt; 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 &amp; 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 &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
<li>Jaime Arroyo &lt;<a class="reference external" href="mailto:jaime.arroyo&#64;creublanca.es">jaime.arroyo&#64;creublanca.es</a>&gt;</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>

View file

@ -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.

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -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.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 137 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -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];
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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,
};
}
}

View file

@ -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};

View file

@ -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>

View file

@ -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";

View file

@ -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;
}
}

View file

@ -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}&amp;field=content&amp;model=dms.file&amp;filename_field=name&amp;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>

View file

@ -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);

View file

@ -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};

View file

@ -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>