[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:
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user