[FIX] web: attach_document widget now works when triggered from a dropdown

Steps to reproduce
==================

- Use a mobile viewport
- Go to Expenses
- Create a record
- Enter a description and a category
- Click on Action
- Click on Attach Receipt
-> Nothing happens

Cause of the issue
==================

When the button is clicked, the dropdown is closed. This means that the
AttachDocumentWidget and FileInput Components are both destroyed.

Solution
========

Instead of using the FileInput Component, use an input element outside
the DOM.

We also use `this.env.services.orm` instead of `useService("orm")`
because the last one does nothing when the widget is destroyed

opw-3065590

closes odoo/odoo#108897

X-original-commit: 84d50b40f150f251a81e128fa9645d7fcc729dab
Signed-off-by: Samuel Degueldre <sad@odoo.com>
This commit is contained in:
Hubert Van de Walle (huvw)
2023-01-02 13:55:50 +01:00
parent 638d0e875d
commit c5e324cbe4
4 changed files with 112 additions and 30 deletions
@@ -7,17 +7,47 @@ import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { Component } from "@odoo/owl";
class AttachDocumentWidget extends Component {
export class AttachDocumentWidget extends Component {
setup() {
this.orm = useService("orm");
this.http = useService("http");
this.notification = useService("notification");
this.fileInput = document.createElement("input");
this.fileInput.type = "file";
this.fileInput.accept = "*";
this.fileInput.multiple = true;
this.fileInput.onchange = this.onInputChange.bind(this);
}
async onInputChange() {
const fileData = await this.http.post(
"/web/binary/upload_attachment",
{
csrf_token: odoo.csrf_token,
ufile: [...this.fileInput.files],
model: this.props.record.resModel,
id: this.props.record.resId,
},
"text"
);
const parsedFileData = JSON.parse(fileData);
if (parsedFileData.error) {
throw new Error(parsedFileData.error);
}
await this.onFileUploaded(parsedFileData);
}
async triggerUpload() {
if (await this.beforeOpen()) {
this.fileInput.click();
}
}
async onFileUploaded(files) {
const { action, record } = this.props;
if (action) {
const { model, resId, resModel } = record;
await this.orm.call(resModel, action, [resId], {
await this.env.services.orm.call(resModel, action, [resId], {
attachment_ids: files.map((file) => file.id),
});
await record.load();
@@ -2,18 +2,8 @@
<template>
<t t-name="web.AttachDocument" owl="1">
<button t-attf-class="btn o_attachment_button #{props.highlight ? 'btn-primary' : 'btn-secondary'}">
<FileInput
onUpload.bind="onFileUploaded"
resId="props.record.resId || undefined"
resModel="props.record.resModel"
beforeOpen.bind="beforeOpen"
multiUpload="true"
>
<t t-set-slot="default">
<span class="o_attach_document" t-esc="props.string"/>
</t>
</FileInput>
<button t-attf-class="btn o_attachment_button #{props.highlight ? 'btn-primary' : 'btn-secondary'}" t-on-click="() => this.triggerUpload()">
<span class="o_attach_document" t-esc="props.string"/>
</button>
</t>
@@ -11,6 +11,7 @@ import {
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { AttachDocumentWidget } from "@web/views/widgets/attach_document/attach_document";
let fixture;
let serverData;
@@ -430,4 +431,52 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
assert.strictEqual(window.screenLeft, 0, "Should be 0 px from left as it is");
}
);
QUnit.test("attach_document widget also works inside a dropdown", async (assert) => {
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
this._super();
fileInput = this.fileInput;
},
});
serviceRegistry.add("http", {
start: () => ({
post: (route, params) => {
assert.step("post");
assert.strictEqual(route, "/web/binary/upload_attachment");
assert.strictEqual(params.model, "partner");
assert.strictEqual(params.id, 1);
return '[{ "id": 5 }, { "id": 2 }]';
},
}),
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<button string="Confirm" />
<widget name="attach_document" string="Attach Document"/>
</header>
<sheet>
<group>
<button name="display_name" />
</group>
</sheet>
</form>
`,
});
await click(fixture, ".o_statusbar_buttons .dropdown-toggle");
await click(fixture, ".o_attach_document");
fileInput.dispatchEvent(new Event("change"));
await nextTick();
assert.verifySteps(["post"]);
});
});
@@ -1,8 +1,15 @@
/** @odoo-module **/
import { click, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
import {
click,
editInput,
getFixture,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
import { AttachDocumentWidget } from "@web/views/widgets/attach_document/attach_document";
const serviceRegistry = registry.category("services");
@@ -35,6 +42,14 @@ QUnit.module("Widgets", (hooks) => {
QUnit.module("AttachDocument");
QUnit.test("attach document widget calls action with attachment ids", async function (assert) {
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
this._super();
fileInput = this.fileInput;
},
});
serviceRegistry.add("http", {
start: () => ({
post: (route, params) => {
@@ -77,19 +92,22 @@ QUnit.module("Widgets", (hooks) => {
await editInput(target, "[name='display_name'] input", "yop");
await click(target, ".o_attach_document");
await triggerEvent(
target,
".o_file_input input",
"change",
{},
{ skipVisibilityCheck: true }
);
fileInput.dispatchEvent(new Event("change"));
await nextTick();
assert.verifySteps(["write", "read", "post", "my_action", "read"]);
});
QUnit.test(
"attach document widget calls action with attachment ids on a new record",
async function (assert) {
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
this._super();
fileInput = this.fileInput;
},
});
serviceRegistry.add("http", {
start: () => ({
post: (route, params) => {
@@ -131,13 +149,8 @@ QUnit.module("Widgets", (hooks) => {
await editInput(target, "[name='display_name'] input", "yop");
await click(target, ".o_attach_document");
await triggerEvent(
target,
".o_file_input input",
"change",
{},
{ skipVisibilityCheck: true }
);
fileInput.dispatchEvent(new Event("change"));
await nextTick();
assert.verifySteps(["create", "read", "post", "my_action", "read"]);
}
);