[IMP] web: allow kanban folded columns examples

Will add the ability to make grouped kanban column examples
with an initial folded status.

**Usage**
Add your kanban column examples with the following options:

```js
import { registry } from "@web/core/registry";

const kanbanExamplesRegistry = registry.category("kanban_examples");
const myExamples = "my-examples";

kanbanExamplesRegistry.add(myExamples, {
  // ...
  allowedGroupBys: ["stage_id"],
  foldField: "fold_state",
  examples: [
    // ...
    { columns: ["To Do"], foldedColumns: ["Done"], /* ... */ },
  ],
});
```

In your arch description:
```xml
<kanban examples="my-examples" ...
```

Your python model should look something like this:
```python
from odoo import fields, models

class Project(models.Model):
  _name = 'project.project'
  stage_id = fields.Many2one('project.project.stage')
  # ...

class ProjectStage(models.Model):
  _name = 'project.project.stage'
  _fold_name = 'fold_state'  # optional, default is 'fold'
  fold_state = fields.Boolean()
  # ...
```

closes odoo/odoo#115909

Taskid: 3246042
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Bruno Boi
2023-03-29 09:33:35 +02:00
parent d2575b50eb
commit 4e974e5977
6 changed files with 94 additions and 20 deletions
@@ -9,10 +9,15 @@ import { Component, useRef } from "@odoo/owl";
const random = (min, max) => Math.floor(Math.random() * (max - min) + min); const random = (min, max) => Math.floor(Math.random() * (max - min) + min);
class KanbanExamplesNotebookTemplate extends Component { class KanbanExamplesNotebookTemplate extends Component {
static defaultProps = {
columns: [],
foldedColumns: [],
};
setup() { setup() {
this.columns = []; this.columns = [];
const hasBullet = this.props.bullets && this.props.bullets.length; const hasBullet = this.props.bullets && this.props.bullets.length;
for (const title of this.props.columns) { const allColumns = [...this.props.columns, ...this.props.foldedColumns];
for (const title of allColumns) {
const col = { title, records: [] }; const col = { title, records: [] };
this.columns.push(col); this.columns.push(col);
for (let i = 0; i < random(1, 5); i++) { for (let i = 0; i < random(1, 5); i++) {
@@ -76,9 +76,14 @@ export class KanbanColumnQuickCreate extends Component {
applyExamplesText: applyExamplesText:
this.props.exampleData.applyExamplesText || this.env._t("Use This For My Kanban"), this.props.exampleData.applyExamplesText || this.env._t("Use This For My Kanban"),
applyExamples: (index) => { applyExamples: (index) => {
for (const groupName of this.props.exampleData.examples[index].columns) { const { examples, foldField } = this.props.exampleData;
const { columns, foldedColumns = [] } = examples[index];
for (const groupName of columns) {
this.props.onValidate(groupName); this.props.onValidate(groupName);
} }
for (const groupName of foldedColumns) {
this.props.onValidate(groupName, { [foldField]: true }, true);
}
}, },
}); });
} }
@@ -51,13 +51,17 @@ export function canQuickCreate(list) {
} }
function validateColumnQuickCreateExamples(data) { function validateColumnQuickCreateExamples(data) {
const { allowedGroupBys = [], examples = [] } = data; const { allowedGroupBys = [], examples = [], foldField = "" } = data;
if (!allowedGroupBys.length) { if (!allowedGroupBys.length) {
throw new Error("The example data must contain an array of allowed groupbys"); throw new Error("The example data must contain an array of allowed groupbys");
} }
if (!examples.length) { if (!examples.length) {
throw new Error("The example data must contain an array of examples"); throw new Error("The example data must contain an array of examples");
} }
const someHasFoldedColumns = examples.some(({ foldedColumns = [] }) => foldedColumns.length);
if (!foldField && someHasFoldedColumns) {
throw new Error("The example data must contain a fold field if there are folded columns");
}
} }
export class KanbanRenderer extends Component { export class KanbanRenderer extends Component {
@@ -116,7 +116,7 @@
<KanbanColumnQuickCreate <KanbanColumnQuickCreate
folded="state.columnQuickCreateIsFolded" folded="state.columnQuickCreateIsFolded"
onFoldChange="folded => state.columnQuickCreateIsFolded = props.list.groups.length > 0 and folded" onFoldChange="folded => state.columnQuickCreateIsFolded = props.list.groups.length > 0 and folded"
onValidate="name => props.list.createGroup(name)" onValidate="props.list.createGroup.bind(props.list)"
exampleData="exampleData" exampleData="exampleData"
groupByField="props.list.groupByField" groupByField="props.list.groupByField"
/> />
+11 -11
View File
@@ -2272,8 +2272,8 @@ export class DynamicGroupList extends DynamicList {
/** /**
* @see {_createGroup} * @see {_createGroup}
*/ */
async createGroup(groupName) { async createGroup(groupName, groupData, isFolded) {
await this.model.mutex.exec(() => this._createGroup(groupName)); await this.model.mutex.exec(() => this._createGroup(groupName, groupData, isFolded));
} }
/** /**
@@ -2413,27 +2413,27 @@ export class DynamicGroupList extends DynamicList {
/** /**
* @param {string} groupName * @param {string} groupName
* @param {{ [relatedFieldName: string]: any }} [groupData={}]
* @param {boolean} [isFolded=false]
* @returns {Promise<Group>} * @returns {Promise<Group>}
*/ */
async _createGroup(groupName) { async _createGroup(groupName, groupData = {}, isFolded = false) {
const [id, displayName] = await this.model.orm.call( groupData = { ...groupData, name: groupName };
this.groupByField.relation, const [id] = await this.model.orm.create(this.groupByField.relation, [groupData], {
"name_create", context: this.context,
[groupName], });
{ context: this.context }
);
const [lastGroup] = this.groups.slice(-1); const [lastGroup] = this.groups.slice(-1);
const group = this.model.createDataPoint("group", { const group = this.model.createDataPoint("group", {
...this.commonGroupParams, ...this.commonGroupParams,
count: 0, count: 0,
value: id, value: id,
displayName, displayName: groupName,
aggregates: {}, aggregates: {},
groupByField: this.groupByField, groupByField: this.groupByField,
groupDomain: Domain.and([this.domain, [[this.groupByField.name, "=", id]]]).toList(), groupDomain: Domain.and([this.domain, [[this.groupByField.name, "=", id]]]).toList(),
isFolded,
rawContext: this.rawContext, rawContext: this.rawContext,
}); });
group.isFolded = false;
this.addGroup(group); this.addGroup(group);
if (lastGroup) { if (lastGroup) {
@@ -2249,7 +2249,7 @@ QUnit.module("Views", (hooks) => {
"web_search_read", // initial search_read (second column) "web_search_read", // initial search_read (second column)
"get_views", // get form view "get_views", // get form view
"onchange", // quick create "onchange", // quick create
"create", // should perform a name_create to create the record "create", // should perform a create to create the record
"onchange", // reopen the quick create automatically "onchange", // reopen the quick create automatically
"read", // read the created record "read", // read the created record
]); ]);
@@ -4292,7 +4292,7 @@ QUnit.module("Views", (hooks) => {
"</kanban>", "</kanban>",
groupBy: ["product_id"], groupBy: ["product_id"],
async mockRPC(route, { method, model }) { async mockRPC(route, { method, model }) {
if (method === "name_create" && model === "product") { if (method === "create" && model === "product") {
await prom; await prom;
} }
}, },
@@ -5687,7 +5687,7 @@ QUnit.module("Views", (hooks) => {
"</kanban>", "</kanban>",
groupBy: ["product_id"], groupBy: ["product_id"],
async mockRPC(route, { method }) { async mockRPC(route, { method }) {
if (method === "name_create" || route === "/web/dataset/resequence") { if (method === "create" || route === "/web/dataset/resequence") {
assert.step(method || route); assert.step(method || route);
} }
}, },
@@ -5735,7 +5735,7 @@ QUnit.module("Views", (hooks) => {
"o_column_folded", "o_column_folded",
"the created column should not be folded" "the created column should not be folded"
); );
assert.verifySteps(["name_create", "/web/dataset/resequence"]); assert.verifySteps(["create", "/web/dataset/resequence"]);
// fold and unfold the created column, and check that no RPCs are done (as there are no records) // fold and unfold the created column, and check that no RPCs are done (as there are no records)
const clickColumnAction = await toggleColumnActions(2); const clickColumnAction = await toggleColumnActions(2);
@@ -6124,7 +6124,7 @@ QUnit.module("Views", (hooks) => {
); );
assert.deepEqual(resequencedIDs, [4, 3], "moved column should be resequenced accordingly"); assert.deepEqual(resequencedIDs, [4, 3], "moved column should be resequenced accordingly");
assert.verifySteps(["name_create"]); assert.verifySteps(["create"]);
}); });
QUnit.test("create a column, delete it and create another one", async (assert) => { QUnit.test("create a column, delete it and create another one", async (assert) => {
@@ -6552,6 +6552,66 @@ QUnit.module("Views", (hooks) => {
); );
}); });
QUnit.test("quick create column and examples: with folded columns", async (assert) => {
serverData.models.partner.records = [];
serverData.models.product.fields.folded = { string: "Folded", type: "boolean" };
serviceRegistry.add("dialog", dialogService, { force: true });
registry.category("kanban_examples").add("test", {
allowedGroupBys: ["product_id"],
foldField: "folded",
examples: [
{
name: "A first example",
columns: ["not folded"],
foldedColumns: ["folded"],
description: "A weak description.",
},
],
});
const expectedGroups = ['[[{"name":"not folded"}]]', '[[{"folded":true,"name":"folded"}]]'];
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: /* xml */ `
<kanban examples="test">
<field name="product_id"/>
<templates><t t-name="kanban-box">
<div><field name="foo"/></div>
</t></templates>
</kanban>
`,
groupBy: ["product_id"],
mockRPC(route, args) {
if (args?.method === "create") {
assert.step(`${args.method} (model: ${args.model})`);
assert.strictEqual(JSON.stringify(args.args), expectedGroups.shift());
}
},
});
// the quick create should already be unfolded as there are no records
assert.containsOnce(target, ".o_column_quick_create .o_quick_create_unfolded");
// click to see the examples
await click(target, ".o_column_quick_create .o_kanban_examples");
// apply the examples
assert.verifySteps([]);
await click(document.body, ".modal .modal-footer .btn.btn-primary");
assert.verifySteps(["create (model: product)", "create (model: product)"]);
// the applied examples should be visible
assert.containsN(target, ".o_kanban_group", 2);
assert.containsOnce(target, ".o_kanban_group:not(.o_column_folded)");
assert.containsOnce(target, ".o_kanban_group.o_column_folded");
assert.deepEqual(
[...target.querySelectorAll(".o_kanban_group")].map((e) => e.textContent),
["not folded", "folded (0)"]
);
});
QUnit.test("quick create column's apply button's display text", async (assert) => { QUnit.test("quick create column's apply button's display text", async (assert) => {
serviceRegistry.add("dialog", dialogService, { force: true }); serviceRegistry.add("dialog", dialogService, { force: true });
const applyExamplesText = "Use This For My Test"; const applyExamplesText = "Use This For My Test";