[IMP] project: update closed_subtask_count when state of subtask changed

Before this commit, when the user displays the subtasks list on a kanban
card and change the state of a subtask to set to Done then the
`closed_subtask_count` field does not changed in the kanban card.

This commit updates the `closed_subtask_count` when the subtask will
be closed or opened on the subtask list displayed in the kanban card.
Also, the `this.props.record.activeFields` is no longer used to fill in
the activeFields of Record component because the `activeFields` will
change in the future.

task-3248592

Part-of: odoo/odoo#120508
This commit is contained in:
Xavier Bol (xbo)
2023-05-12 19:54:49 +02:00
parent af69310b38
commit 93b90a3ff3
3 changed files with 115 additions and 71 deletions
@@ -1,28 +1,33 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { Component, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { Record } from "@web/views/record";
import { KanbanMany2ManyTagsAvatarUserField } from "@mail/web/fields/many2many_avatar_user_field/many2many_avatar_user_field";
import { Field } from "@web/views/fields/field";
import { Field, getFieldFromRegistry } from "@web/views/fields/field";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
export class SubtaskKanbanList extends Component {
setup() {
this.actionService = useService("action");
this.orm = useService("orm");
this.subTaskIds = [];
this.subTasksRead = [];
this.subTaskClosed = new Set();
onWillStart(this.onWillStart);
}
async onWillStart() {
this.subTaskIds = await this.orm.search(this.props.record.resModel, [
["parent_id", "=", this.props.record.resId],
["state", "not in", ["1_done", "1_canceled"]],
]);
this.subTasksRead = await this.orm.searchRead(
this.props.record.resModel, [
["parent_id", "=", this.props.record.resId],
["state", "not in", ["1_done", "1_canceled"]],
],
this.fieldNames
);
}
async goToSubtask(subtask_id) {
@@ -38,44 +43,44 @@ export class SubtaskKanbanList extends Component {
});
}
get fieldsInfo() {
get fieldNames() {
return Object.keys(this.fields);
}
get fields() {
const { display_name, state, user_ids } = this.props.record.fields;
return {
child_ids: {
type: "one2many",
relation: "project.task",
relatedFields: {
display_name: { type: "char" },
state: {
selection: [
["01_in_progress", "In Progress"],
["02_changes_requested", "Changes Requested"],
["03_approved", "Approved"],
["04_waiting_normal", "Waiting"],
["1_done", "Done"],
["1_canceled", "Canceled"],
],
string: "Status",
type: "selection",
field: this.props.record.activeFields.state.field,
viewType: this.props.record.activeFields.state.viewType,
attrs: this.props.record.activeFields.state.attrs,
options: this.props.record.activeFields.state.options,
},
user_ids: {
type: "many2many",
relation: "res.users",
field: this.props.record.activeFields.user_ids.field,
relatedFields: this.props.record.activeFields.user_ids.relatedFields,
attrs: this.props.record.activeFields.user_ids.attrs,
options: this.props.record.activeFields.user_ids.options,
},
},
display_name,
state,
user_ids,
};
}
get activeFields() {
return {
display_name: {},
state: {
viewType: "kanban",
field: getFieldFromRegistry(this.fields.state.type, "project_task_state_selection", "kanban"),
},
user_ids: {
field: getFieldFromRegistry(this.fields.user_ids.type, "many2many_avatar_user", "kanban"),
},
};
}
get values() {
return { child_ids: this.subTaskIds };
async onSubTaskSaved(subTask) {
const isKnownAsClosed = this.subTaskClosed.has(subTask.resId);
const isClosed = subTask.data.state.startsWith("1_");
if (isKnownAsClosed && !isClosed) {
this.subTaskClosed.delete(subTask.resId);
} else if (!isKnownAsClosed && isClosed) {
this.subTaskClosed.add(subTask.resId);
} else { // nothing to do
return;
}
await this.props.record.load();
this.props.record.model.notify();
}
}
@@ -3,31 +3,38 @@
<templates>
<t t-name="project.SubtaskKanbanList" owl="1">
<Record resModel="'project.task'" resId="props.record.resId" fields="fieldsInfo" activeFields="fieldsInfo" values="values" t-slot-scope="data">
<div class="subtask_list">
<t t-foreach="data.record.data.child_ids.records" t-as="subTask" t-key="subTask.resId">
<div class="subtask_list">
<t t-foreach="subTasksRead" t-as="subTask" t-key="subTask.id">
<Record resModel="'project.task'"
resId="subTask.id"
fields="fields"
activeFields="activeFields"
values="subTask"
t-slot-scope="data"
onRecordSaved.bind="onSubTaskSaved"
>
<div class="subtask_list_row">
<a t-attf-class="subtask_name_col {{['1_done', '1_canceled'].includes(subTask.data.state) ? 'text-muted opacity-50' : ''}}"
t-att-title="subTask.data.display_name"
<a t-attf-class="subtask_name_col {{['1_done', '1_canceled'].includes(data.record.data.state) ? 'text-muted opacity-50' : ''}}"
t-att-title="data.record.data.display_name"
style="color: inherit;"
t-esc="subTask.data.display_name"
t-on-click.prevent="() => this.goToSubtask(subTask.resId)"/>
t-esc="data.record.data.display_name"
t-on-click.prevent="() => this.goToSubtask(data.record.resId)"/>
<Field
class="`subtask_user_widget_col justify-content-end align-items-center ${['1_done', '1_canceled'].includes(subTask.data.state) ? 'opacity-50' : ''}`"
name="'user_ids'"
record="subTask"
fieldInfo="subTask.fields.user_ids"
readonly="true"
type="'many2many_avatar_user'"/>
class="`subtask_user_widget_col justify-content-end align-items-center ${['1_done', '1_canceled'].includes(data.record.data.state) ? 'opacity-50' : ''}`"
name="'user_ids'"
record="data.record"
fieldInfo="data.record.activeFields.user_ids"
readonly="true"
type="'many2many_avatar_user'"/>
<Field name="'state'"
class="`subtask_state_widget_col d-flex justify-content-center align-items-center`"
record="subTask"
fieldInfo="subTask.fields.state"
type="'project_task_state_selection'"/>
class="`subtask_state_widget_col d-flex justify-content-center align-items-center`"
record="data.record"
fieldInfo="data.record.activeFields.state"
type="'project_task_state_selection'"/>
</div>
</t>
</div>
</Record>
</Record>
</t>
</div>
</t>
</templates>
@@ -24,12 +24,13 @@ QUnit.module('Subtask Kanban List tests', {
{ name: "User One", login: 'one', password: 'one' },
]);
pyEnv['project.task'].create([
{ name: 'task one', project_id: projectId, closed_subtask_count: 2, subtask_count: 3, child_ids: [2, 3, 4], state: '01_in_progress', user_ids: [userId] },
{ name: 'task one', project_id: projectId, closed_subtask_count: 1, subtask_count: 4, child_ids: [2, 3, 4, 7], state: '01_in_progress', user_ids: [userId] },
{ name: 'task two', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '03_approved' },
{ name: 'task three', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '02_changes_requested' },
{ name: 'task four', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '1_done' },
{ name: 'task five', closed_subtask_count: 0, subtask_count: 1, child_ids: [6], state: '03_approved' },
{ name: 'task six', parent_id: 5, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '1_canceled' },
{ name: 'task seven', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '01_in_progress', user_ids: [userId] },
]);
this.views = {
"project.task,false,kanban":
@@ -39,15 +40,15 @@ QUnit.module('Subtask Kanban List tests', {
<templates>
<t t-name="kanban-box">
<div>
<field name="name" widget="name_with_subtask_count"/>
<field name="user_ids" invisible="1"/>
<field name="state" invisible="1"/>
<field name="name"/>
<field name="user_ids" invisible="1" widget="many2many_avatar_user"/>
<field name="state" invisible="1" widget="project_task_state_selection"/>
<a t-if="record.closed_subtask_count.raw_value">
<span title="See Subtasks" class="subtask_list_button fa fa-check-square-o me-1"/>
<t t-out="record.closed_subtask_count.value"/>/<t t-out="record.subtask_count.value"/>
</a>
<div class="kanban_bottom_subtasks_section"/>
</div>
<div class="kanban_bottom_subtasks_section"/>
</t>
</templates>
</kanban>`,
@@ -72,13 +73,44 @@ QUnit.module('Subtask Kanban List tests', {
await click(target, '.subtask_list_button');
assert.containsOnce(target, '.subtask_list', "Clicking on the button should make the subtask list render, in this case we are expectig 1 list");
assert.containsN(target, '.subtask_list_row', 2, "The list rendered should show the open subtasks of the task, in this case 2");
assert.containsN(target, '.subtask_state_widget_col', 2, "Each of the list's rows should have 1 state widget, thus we are looking for 2 in total");
assert.containsN(target, '.subtask_user_widget_col', 2, "Each of the list's rows should have 1 user widgets, thus we are looking for 2 in total");
assert.containsN(target, '.subtask_name_col', 2, "Each of the list's rows should display the subtask's name, thus we are looking for 2 in total");
assert.containsN(target, '.subtask_list_row', 3, "The list rendered should show the open subtasks of the task, in this case 3");
assert.containsN(target, '.subtask_state_widget_col', 3, "Each of the list's rows should have 1 state widget, thus we are looking for 3 in total");
assert.containsN(target, '.subtask_user_widget_col', 3, "Each of the list's rows should have 1 user widgets, thus we are looking for 3 in total");
assert.containsN(target, '.subtask_name_col', 3, "Each of the list's rows should display the subtask's name, thus we are looking for 3 in total");
await click(target, '.subtask_list_button');
assert.containsNone(target, '.subtask_list', "If the drawdown button is clicked again, the subtasks list should be hidden again");
});
QUnit.test("Update closed subtask count in the kanban card when the state of a subtask is set to Done.", async function (assert) {
let checkSteps = false;
const { openView } = await start({
serverData: { views: this.views },
mockRPC(route, { model, method }) {
if (checkSteps) {
assert.step(`${model}/${method}`);
}
},
});
await openView({
res_model: "project.task",
views: [[false, "kanban"]],
});
assert.strictEqual(target.querySelector(".subtask_list_button").parentElement.textContent, "1/4");
checkSteps = true;
await click(target, '.subtask_list_button');
const subtaskEl = target.querySelector(".subtask_list");
assert.containsOnce(subtaskEl, ".o_field_widget.o_field_project_task_state_selection.subtask_state_widget_col .o_status:not(.o_status_green,.o_status_bubble)", "The state of the subtask should be in progress");
await click(subtaskEl, ".o_field_widget.o_field_project_task_state_selection.subtask_state_widget_col .o_status:not(.o_status_green,.o_status_bubble)");
assert.containsNone(subtaskEl, ".o_field_widget.o_field_project_task_state_selection.subtask_state_widget_col .o_status:not(.o_status_green,.o_status_bubble)", "The state of the subtask should no longer be in progress");
assert.containsOnce(subtaskEl, ".o_field_widget.o_field_project_task_state_selection.subtask_state_widget_col .fa-check-circle.text-success", "The state of the subtask should be Done");
assert.verifySteps([
"project.task/search_read",
"project.task/write",
"project.task/read",
"project.task/read", // read the parent task to recompute the subtask count
]);
});
});