[FIX] web: Fix KanbanCoverImageDialog
This commit removes the SimpleDialog class and adapts the KanbanCoverImageDialog to use the Dialog component instead. It also makes sure that the KanbanCoverImageDialog uses the FileInput component to use a more centralized way of calling the HTTP 'upload_attachment' route. Part-of: odoo/odoo#92475
This commit is contained in:
committed by
Aaron Bohy
parent
38710465a9
commit
5d0a28d0f5
@@ -54,19 +54,3 @@ Dialog.defaultProps = {
|
||||
technical: true,
|
||||
title: "Odoo",
|
||||
};
|
||||
|
||||
export class SimpleDialog extends Component {
|
||||
setup() {
|
||||
useActiveElement("modal");
|
||||
useHotkey("escape", () => {
|
||||
this.props.close();
|
||||
});
|
||||
useChildSubEnv({ inDialog: true });
|
||||
}
|
||||
}
|
||||
SimpleDialog.template = "web.SimpleDialog";
|
||||
SimpleDialog.props = {
|
||||
close: Function,
|
||||
isActive: { optional: true },
|
||||
"*": true,
|
||||
};
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
<!-- FIXME: WOWL there is a bug on t-portal on owl, in which t-portal don't work on multinode.
|
||||
To avoid this we place the footer before the body -->
|
||||
<footer t-if="props.footer" class="modal-footer justify-content-around justify-content-sm-start flex-wrap gap-1" style="order:2">
|
||||
<t t-slot="footer">
|
||||
<t t-slot="footer" close="data.close">
|
||||
<button class="btn btn-primary o-default-button" t-on-click="data.close">
|
||||
<t>Ok</t>
|
||||
</button>
|
||||
</t>
|
||||
</footer>
|
||||
<main class="modal-body">
|
||||
<t t-slot="default"/>
|
||||
<t t-slot="default" close="data.close" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.SimpleDialog" owl="1">
|
||||
<div class="o_dialog" t-att-class="{ o_inactive_modal: !props.isActive }" t-on-dialog-closed="props.close">
|
||||
<div role="dialog" class="modal d-block"
|
||||
tabindex="-1"
|
||||
t-att-class="{ o_technical_modal: props.technical, o_modal_full: props.fullscreen }"
|
||||
t-ref="modal"
|
||||
>
|
||||
<div class="modal-dialog" t-att-class="props.size">
|
||||
<div class="modal-content" t-att-class="props.contentClass">
|
||||
<header t-if="!props.hideHeader" class="modal-header">
|
||||
<h4 class="modal-title text-break">
|
||||
<t t-esc="props.title"/>
|
||||
</h4>
|
||||
<button type="button" class="close" aria-label="Close" tabindex="-1" t-on-click="props.close">×</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<t t-slot="default"/>
|
||||
</main>
|
||||
<footer t-if="props.slots.footer" class="modal-footer justify-content-start">
|
||||
<t t-slot="footer" close="props.close"/>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -99,11 +99,11 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
|
||||
const nodeParams = extractAttributes(el, ["type"]);
|
||||
if (type === "set_cover") {
|
||||
const { "data-field": fieldName, "auto-open": autoOpen } = extractAttributes(el, [
|
||||
"data-field",
|
||||
const { "auto-open": autoOpen, "data-field": fieldName } = extractAttributes(el, [
|
||||
"auto-open",
|
||||
"data-field",
|
||||
]);
|
||||
Object.assign(nodeParams, { fieldName, autoOpen });
|
||||
Object.assign(nodeParams, { autoOpen, fieldName });
|
||||
}
|
||||
const strParams = Object.entries(nodeParams)
|
||||
.map(([k, v]) => [k, toStringExpression(v)].join(":"))
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { SimpleDialog } from "@web/core/dialog/dialog";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, useState, useRef, onWillStart, onMounted } = owl;
|
||||
const { Component, useState, onWillStart } = owl;
|
||||
|
||||
let nextDialogId = 1;
|
||||
|
||||
@@ -11,12 +12,13 @@ export class KanbanCoverImageDialog extends Component {
|
||||
setup() {
|
||||
this.id = `o_cover_image_upload_${nextDialogId++}`;
|
||||
this.orm = useService("orm");
|
||||
this.http = useService("http");
|
||||
const { record, fieldName } = this.props;
|
||||
this.coverId = record && record.data[fieldName];
|
||||
const attachment = (record && record.data[fieldName]) || [];
|
||||
this.state = useState({
|
||||
selectedAttachmentId: this.coverId,
|
||||
selectFile: false,
|
||||
selectedAttachmentId: attachment[0],
|
||||
});
|
||||
this.fileInput = useRef("fileInput");
|
||||
onWillStart(async () => {
|
||||
this.attachments = await this.orm.searchRead(
|
||||
"ir.attachment",
|
||||
@@ -25,22 +27,29 @@ export class KanbanCoverImageDialog extends Component {
|
||||
["res_id", "=", record.resId],
|
||||
["mimetype", "ilike", "image"],
|
||||
],
|
||||
["id", "name"]
|
||||
["id"]
|
||||
);
|
||||
});
|
||||
onMounted(() => {
|
||||
if (!this.props.autoOpen && this.attachments.length === 0) {
|
||||
this.fileInput.el.click();
|
||||
}
|
||||
this.state.selectFile = this.props.autoOpen && this.attachments.length;
|
||||
});
|
||||
}
|
||||
|
||||
selectAttachment(attachment) {
|
||||
onUpload([attachment]) {
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
this.state.selectFile = false;
|
||||
this.selectAttachment(attachment, true);
|
||||
}
|
||||
|
||||
selectAttachment(attachment, setSelected) {
|
||||
if (this.state.selectedAttachmentId !== attachment.id) {
|
||||
this.state.selectedAttachmentId = attachment.id;
|
||||
} else {
|
||||
this.state.selectedAttachmentId = null;
|
||||
}
|
||||
if (setSelected) {
|
||||
this.setCover();
|
||||
}
|
||||
}
|
||||
|
||||
removeCover() {
|
||||
@@ -54,7 +63,11 @@ export class KanbanCoverImageDialog extends Component {
|
||||
await this.props.record.save();
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
uploadImage() {
|
||||
this.state.selectFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
KanbanCoverImageDialog.template = "web.KanbanCoverImageDialog";
|
||||
KanbanCoverImageDialog.components = { SimpleDialog };
|
||||
KanbanCoverImageDialog.components = { Dialog, FileInput };
|
||||
|
||||
@@ -2,38 +2,45 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.KanbanCoverImageDialog" owl="1">
|
||||
<SimpleDialog t-props="props" title="'Set a Cover Image'" size="'modal-lg'">
|
||||
<Dialog title="'Set a Cover Image'" size="'lg'">
|
||||
<div t-if="attachments.length" class="o_kanban_cover_container bg-100">
|
||||
<t t-foreach="attachments" t-as="attachment" t-key="attachment.id">
|
||||
<div class="o_kanban_cover_image position-relative d-inline-block m-2 bg-white" t-att-class="{
|
||||
o_selected: state.selectedAttachmentId === attachment.id,
|
||||
}">
|
||||
<img t-attf-src="/web/image/{{attachment.id}}?unique=1" t-att-data-id="attachment.id" t-att-data-name="attachment.name" class="m-auto mw-100" alt="Attachment" t-on-click="() => this.selectAttachment(attachment)" t-on-dblclick="() => {this.selectAttachment(attachment); this.setCover();}"/>
|
||||
<img t-attf-src="/web/image/{{attachment.id}}?unique=1"
|
||||
class="m-auto mw-100"
|
||||
alt="Attachment"
|
||||
t-on-click="() => this.selectAttachment(attachment, false)"
|
||||
t-on-dblclick="() => this.selectAttachment(attachment, true)"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-else="">There is no available image to be set as cover.</t>
|
||||
<div t-attf-class="d-none o_hidden_input_file {{fileupload_class}}" t-att-style="fileupload_style" aria-atomic="true">
|
||||
<form class="o_form_binary_form" t-att-target="id"
|
||||
method="post" enctype="multipart/form-data" t-att-action="'/web/binary/upload_attachment'">
|
||||
<input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
|
||||
<input type="hidden" name="callback" t-att-value="id"/>
|
||||
<input type="file" class="o_input_file" name="ufile "
|
||||
t-att-multiple="multi_upload ? 'multiple' : null" accept="image/*" t-ref="fileInput" t-on-change="upload"/>
|
||||
<input type="hidden" name="model" t-att-value="props.record.resModel" />
|
||||
<input type="hidden" name="id" t-att-value="props.record.resId" />
|
||||
</form>
|
||||
<iframe t-att-id="id" t-att-name="id" style="display: none"/>
|
||||
</div>
|
||||
<FileInput t-if="state.selectFile"
|
||||
acceptedFileExtensions="'image/*'"
|
||||
autoOpen="true"
|
||||
hidden="true"
|
||||
onUpload.bind="onUpload"
|
||||
resModel="props.record.resModel"
|
||||
resId="props.record.resId"
|
||||
/>
|
||||
<t t-set-slot="footer" t-slot-scope="dialog">
|
||||
<t t-if="attachments.length">
|
||||
<button class="btn btn-primary" t-on-click="setCover" t-att-disabled="!state.selectedAttachmentId">Select</button>
|
||||
<button class="btn btn-primary" t-on-click="uploadImage">Upload and Set</button>
|
||||
</t>
|
||||
<button t-if="coverId" class="btn btn-secondary" t-on-click="removeCover">Remove Cover Image</button>
|
||||
<button class="btn btn-secondary" t-on-click="dialog.close">Discard</button>
|
||||
<button t-if="attachments.length" class="btn btn-primary" t-on-click="setCover" t-att-disabled="!state.selectedAttachmentId">
|
||||
Select
|
||||
</button>
|
||||
<button class="btn btn-primary" t-on-click="uploadImage">
|
||||
Upload and Set
|
||||
</button>
|
||||
<button t-if="coverId" class="btn btn-secondary" t-on-click="removeCover">
|
||||
Remove Cover Image
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="dialog.close">
|
||||
Discard
|
||||
</button>
|
||||
</t>
|
||||
</SimpleDialog>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -317,7 +317,7 @@ export class KanbanRecord extends Component {
|
||||
return;
|
||||
}
|
||||
case "set_cover": {
|
||||
const { fieldName, autoOpen } = params;
|
||||
const { autoOpen, fieldName } = params;
|
||||
const { widget } = archInfo.fieldNodes[fieldName];
|
||||
const field = record.fields[fieldName];
|
||||
if (
|
||||
@@ -325,7 +325,7 @@ export class KanbanRecord extends Component {
|
||||
field.relation === "ir.attachment" &&
|
||||
widget === "attachment_image"
|
||||
) {
|
||||
this.dialog.add(KanbanCoverImageDialog, { record, fieldName, autoOpen });
|
||||
this.dialog.add(KanbanCoverImageDialog, { autoOpen, fieldName, record });
|
||||
} else {
|
||||
const warning = sprintf(
|
||||
env._t(
|
||||
|
||||
@@ -8723,9 +8723,12 @@ QUnit.module("Views", (hooks) => {
|
||||
});
|
||||
|
||||
QUnit.test("set cover image", async (assert) => {
|
||||
assert.expect(7);
|
||||
assert.expect(10);
|
||||
|
||||
serviceRegistry.add("dialog", dialogService, { force: true });
|
||||
serviceRegistry.add("http", {
|
||||
start: () => ({}),
|
||||
});
|
||||
const kanban = await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
@@ -8778,8 +8781,8 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
assert.containsNone(getCard(0), "img", "Initially there is no image.");
|
||||
|
||||
await click(document.body, ".modal img[data-id='1']", true);
|
||||
await click(document.body.querySelector(".modal .btn-primary"));
|
||||
await click(document.body, ".modal .o_kanban_cover_image img", true);
|
||||
await click(document.body, ".modal .btn-primary:first-child");
|
||||
|
||||
assert.containsOnce(target, 'img[data-src*="/web/image/1"]');
|
||||
|
||||
@@ -8787,11 +8790,16 @@ QUnit.module("Views", (hooks) => {
|
||||
const coverButton = getCard(1).querySelector("a");
|
||||
assert.strictEqual(coverButton.innerText.trim(), "Set Cover Image");
|
||||
await click(coverButton);
|
||||
|
||||
assert.containsOnce(document.body, ".modal .o_kanban_cover_image");
|
||||
assert.containsOnce(document.body, ".modal .btn:contains(Select)");
|
||||
assert.containsOnce(document.body, ".modal .btn:contains(Discard)");
|
||||
|
||||
await triggerEvent(
|
||||
document.body,
|
||||
".modal img[data-id='2']",
|
||||
".modal .o_kanban_cover_image img",
|
||||
"dblclick",
|
||||
{},
|
||||
{ bubbles: true },
|
||||
{ skipVisibilityCheck: true }
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user