Skip to content

Commit

Permalink
[ADD] web_m2m_inline
Browse files Browse the repository at this point in the history
  • Loading branch information
trisdoan committed Jan 17, 2025
1 parent 0fe0548 commit 77a5d1e
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 0 deletions.
102 changes: 102 additions & 0 deletions web_m2m_inline/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
==============
Web M2m Inline
==============

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2b35600f7a72d13ee75692f74827b74a0f0e057462a818e3e1eeb21d6851efff
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/17.0/web_m2m_inline
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_m2m_inline
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=17.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module extends the functionality of the Many2Many field in Odoo to
support inline adjustments. It allows users to directly add or edit
Many2Many field values within the form view.

**Table of contents**

.. contents::
:local:

Usage
=====

To use this module, you need to add widget="m2m_inline":

.. code-block:: XML
::

<field name="my_m2m_field_ids" widget="m2m_inline">
<tree editable="bottom">
<field name="name"/>
</tree>
</field>

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
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
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_m2m_inline%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Camptocamp

Contributors
------------

- ``Trobz <https://www.trobz.com>``

- Tris Doan [email protected]

Other credits
-------------

The development of this module has been financially supported by:

- Camptocamp

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

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.

This module is part of the `OCA/web <https://github.com/OCA/web/tree/17.0/web_m2m_inline>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Empty file added web_m2m_inline/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions web_m2m_inline/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2025 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Web M2m Inline",
"summary": """Inline creation/update for M2M field""",
"version": "17.0.1.0.0",
"license": "AGPL-3",
"author": "Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
"depends": ["web"],
"assets": {
"web.assets_backend": [
"web_m2m_inline/static/src/**/*",
],
},
}
3 changes: 3 additions & 0 deletions web_m2m_inline/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
3 changes: 3 additions & 0 deletions web_m2m_inline/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* `Trobz <https://www.trobz.com>`

* Tris Doan <[email protected]>
3 changes: 3 additions & 0 deletions web_m2m_inline/readme/CREDITS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The development of this module has been financially supported by:

- Camptocamp
1 change: 1 addition & 0 deletions web_m2m_inline/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This module extends the functionality of the Many2Many field in Odoo to support inline adjustments. It allows users to directly add or edit Many2Many field values within the form view.
9 changes: 9 additions & 0 deletions web_m2m_inline/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
To use this module, you need to add widget="m2m_inline":

.. code-block:: XML

<field name="my_m2m_field_ids" widget="m2m_inline">
<tree editable="bottom">
<field name="name"/>
</tree>
</field>
Binary file added web_m2m_inline/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions web_m2m_inline/static/src/autocomplete.esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** @odoo-module **/

import {_t} from "@web/core/l10n/translation";
import {Many2XAutocomplete} from "@web/views/fields/relational_utils";

export class CustomMany2XAutocomplete extends Many2XAutocomplete {
async loadOptionsSource(request) {
const res = await super.loadOptionsSource(request);
if (this.props.value) {
const inputVal = this.autoCompleteContainer.el.querySelector("input").value;
const record = await this.orm.call(this.props.resModel, "name_search", [], {
name: this.props.value,
operator: "ilike",
args: [],
limit: 1,
context: this.props.context,
});
res.push({
label: _t(`Edit ${record[0][1]}`),
classList: "o_m2o_dropdown_option o_m2o_dropdown_option_create_edit",
action: () =>
this._updateRecord(this.props.resModel, record[0], inputVal),
});
}
return res;
}
async _updateRecord(model, record, changes) {
return await this.orm.write(model, [record[0]], {name: changes});
}
}
96 changes: 96 additions & 0 deletions web_m2m_inline/static/src/list_renderer.esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/** @odoo-module **/

import {Domain} from "@web/core/domain";
import {useX2ManyCrud} from "@web/views/fields/relational_utils";
import {useService} from "@web/core/utils/hooks";
import {ListRenderer} from "@web/views/list/list_renderer";
import {CustomMany2XAutocomplete} from "./autocomplete";

export class AutoCompleteListRenderer extends ListRenderer {
static components = {
...ListRenderer,
CustomMany2XAutocomplete,
};

setup() {
super.setup();
this.orm = useService("orm");
const {saveRecord, removeRecord} = useX2ManyCrud(() => this.props.list, true);
this.update = (recordlist) => {
if (!recordlist || !Array.isArray(recordlist)) {
return;
}
if (this.selectedRecord) {
// Without removing, this record is kept the list
removeRecord(this.selectedRecord);
}
const resIds = recordlist.map((rec) => rec.id);
saveRecord(resIds);
return this.props.list.leaveEditMode();
};

if (this.props.canQuickCreate) {
this.quickCreate = async (name) => {
const created = await this.orm.call(
this.relation,
"name_create",
[name],
{
context: this.props.context,
}
);
saveRecord([created[0]]);
return this.props.list.leaveEditMode();
};
}
}

get showM2OSelectionField() {
return !this.props.readonly;
}

get relation() {
return this.props.list.records[0].resModel;
}

get string() {
return this.record.fields[this.column.name].string || "";
}

getDomain() {
const domain =
typeof this.props.domain === "function"
? this.props.domain()
: this.props.domain;
const currentIds = this.props.list._currentIds.filter(
(id) => typeof id === "number"
);
return Domain.and([domain, Domain.not([["id", "in", currentIds]])]).toList(
this.props.context
);
}

/**
* Override to store selected record
*/
async onCellClicked(record, column, ev) {
await super.onCellClicked(record, column, ev);
if (!record.isNew) {
this.selectedRecord = record;
}
}
}

AutoCompleteListRenderer.recordRowTemplate =
"c2c_governance.AutoCompleteListRenderer.recordRowTemplate";

AutoCompleteListRenderer.props = [
...ListRenderer.props,
"canCreate?",
"canQuickCreate?",
"canCreateEdit?",
"createDomain?",
"context?",
"domain?",
"readonly?",
];
66 changes: 66 additions & 0 deletions web_m2m_inline/static/src/list_renderer.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" ?>
<t
t-name="c2c_governance.AutoCompleteListRenderer.recordRowTemplate"
t-inherit-mode="primary"
t-inherit="web.ListRenderer.RecordRow"
>
<xpath expr="//t[@t-if=&quot;column.type === 'field'&quot;]" position="replace">
<t t-if="column.type === 'field'">
<t
t-set="isInvisible"
t-value="evalInvisible(column.invisible, record) or !(column.name in record.data)"
/>
<td
t-on-keydown="(ev) =&gt; this.onCellKeydown(ev, group, record)"
class="o_data_cell cursor-pointer"
t-att-class="getCellClass(column, record)"
t-att-name="column.name"
t-att-colspan="column.colspan"
t-att-data-tooltip="!isInvisible ? getCellTitle(column, record) : false"
data-tooltip-delay="1000"
tabindex="-1"
t-on-click="(ev) =&gt; this.onCellClicked(record, column, ev)"
>
<t t-if="!isInvisible">
<t t-if="showM2OSelectionField">
<div
class="o_field_many2many_selection d-inline-flex w-100"
t-ref="autoComplete"
>
<CustomMany2XAutocomplete
id="column.id"
value="record.data.name"
resModel="relation"
autoSelect="true"
fieldString="string"
update="update"
activeActions="activeActions"
quickCreate="quickCreate"
context="props.context"
getDomain.bind="getDomain"
isToMany="true"
nameCreateField="props.nameCreateField"
noSearchMore="false"
/>
</div>
</t>
<t t-else="">
<t
t-if="canUseFormatter(column, record)"
t-out="getFormattedValue(column, record)"
/>
<Field
t-else=""
name="column.name"
record="record"
type="column.widget"
class="getFieldClass(column)"
fieldInfo="column"
t-props="getFieldProps(record, column)"
/>
</t>
</t>
</td>
</t>
</xpath>
</t>
Loading

0 comments on commit 77a5d1e

Please sign in to comment.