[FIX] web: company selector usability

Since 0eff29409d, the behavior was
unpredictable when clicking on multiple companies before a reload.

This simplifies the implementation and the behavior:
* the behavior is always the same even if there were already multiple
  companies selected: if one wants to add new companies they should use
  the check box instead.
* there is no more confusion due to the storage of the symetrical
  difference when toggling multiple companies and their children before
  the reload.

closes odoo/odoo#135086

X-original-commit: b07b30cd4b2a3e821a24761075ae0aecee332b14
Related: odoo/enterprise#47265
Signed-off-by: Bastien Fafchamps (bafa) <bafa@odoo.com>
Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
william
2023-09-11 23:59:51 +00:00
parent 68d89150b1
commit dacbe1d496
4 changed files with 119 additions and 98 deletions
@@ -2,7 +2,6 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { symmetricalDifference } from "../core/utils/arrays";
import { session } from "@web/session";
import { UPDATE_METHODS } from "@web/core/orm_service";
@@ -39,15 +38,29 @@ export const companyService = {
} else if ("cids" in cookie.current) {
cids = parseCompanyIds(cookie.current.cids);
}
const availableCompanies = session.user_companies.allowed_companies;
const allowedCompanyIds = computeAllowedCompanyIds(cids);
const nextAvailableCompanies = allowedCompanyIds.slice(); // not using a Set because order is important
nextAvailableCompanies.add = (companyId) => {
if (!nextAvailableCompanies.includes(companyId)) {
nextAvailableCompanies.push(companyId);
availableCompanies[companyId].child_ids.map(nextAvailableCompanies.add);
}
}
nextAvailableCompanies.remove = (companyId) => {
if (nextAvailableCompanies.includes(companyId)) {
nextAvailableCompanies.splice(nextAvailableCompanies.indexOf(companyId), 1);
availableCompanies[companyId].child_ids.map(nextAvailableCompanies.remove);
}
}
const stringCIds = allowedCompanyIds.join(",");
router.replaceState({ cids: stringCIds }, { lock: true });
cookie.setCookie("cids", stringCIds);
user.updateContext({ allowed_company_ids: allowedCompanyIds });
const availableCompanies = session.user_companies.allowed_companies;
// reload the page if changes are being done to `res.company`
env.bus.addEventListener("RPC:RESPONSE", (ev) => {
const { data, error } = ev.detail;
const { model, method } = data.params;
@@ -57,8 +70,10 @@ export const companyService = {
}
}
});
return {
availableCompanies,
nextAvailableCompanies,
get allowedCompanyIds() {
return allowedCompanyIds.slice();
},
@@ -68,47 +83,24 @@ export const companyService = {
getCompany(companyId) {
return availableCompanies[companyId];
},
getAllChildren(companyId) {
return [
...availableCompanies[companyId].child_ids,
...availableCompanies[companyId].child_ids.map((child) => this.getAllChildren(child))
].flat()
},
getChildrenToToggle(companyId) {
const children = this.getAllChildren(companyId)
if ( allowedCompanyIds.includes(companyId) ) {
return children.filter((id) => allowedCompanyIds.includes(id))
} else {
return children.filter((id) => !allowedCompanyIds.includes(id))
}
},
setCompanies(mode, ...companyIds) {
// compute next company ids
let nextCompanyIds;
setCompanies(mode, companyId) {
if (mode === "toggle") {
nextCompanyIds = symmetricalDifference(allowedCompanyIds, companyIds);
} else if (mode === "loginto") {
const companyId = companyIds[0];
const children = this.getAllChildren(companyId);
if (allowedCompanyIds.length === 1) {
// 1 enabled company: stay in single company mode
nextCompanyIds = [companyId, ...children];
if (nextAvailableCompanies.includes(companyId)) {
nextAvailableCompanies.remove(companyId);
} else {
// multi company mode
nextCompanyIds = [
companyId,
...children,
...allowedCompanyIds.filter((id) => id !== companyId && !children.includes(id)),
];
nextAvailableCompanies.add(companyId);
}
} else if (mode === "loginto") {
nextAvailableCompanies.splice(0, nextAvailableCompanies.length);
nextAvailableCompanies.add(companyId)
}
nextCompanyIds = nextCompanyIds.length ? nextCompanyIds : [companyIds[0]];
// apply them
router.pushState({ cids: nextCompanyIds }, { lock: true });
cookie.setCookie("cids", nextCompanyIds);
browser.setTimeout(() => browser.location.reload()); // history.pushState is a little async
},
logNextCompanies() {
const next = nextAvailableCompanies.length ? nextAvailableCompanies : [allowedCompanyIds[0]]
router.pushState({ cids: next }, { lock: true });
cookie.setCookie("cids", next);
browser.setTimeout(() => browser.location.reload()); // history.pushState is a little async
}
};
},
};
@@ -5,13 +5,12 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { browser } from "@web/core/browser/browser";
import { symmetricalDifference } from "@web/core/utils/arrays";
import { Component, useState, reactive } from "@odoo/owl";
const store = reactive({
companiesToToggle: [],
toggleTimer: null,
nextAvailableCompanies: [],
});
@@ -21,25 +20,25 @@ export class SwitchCompanyItem extends Component {
this.store = useState(store);
}
logIntoCompany(companyId) {
browser.clearTimeout(this.store.toggleTimer);
this.companyService.setCompanies("loginto", companyId);
get selectedCompanies() {
this.store.nextAvailableCompanies; // check if the state changed
return this.companyService.nextAvailableCompanies;
}
get selectedCompanies() {
return symmetricalDifference(
this.companyService.allowedCompanyIds,
this.store.companiesToToggle,
);
logIntoCompany(companyId) {
this.companyService.setCompanies("loginto", companyId);
browser.clearTimeout(this.store.toggleTimer);
this.companyService.logNextCompanies();
}
toggleCompany(companyId) {
this.store.companiesToToggle = symmetricalDifference(this.store.companiesToToggle, [
companyId, ...this.companyService.getChildrenToToggle(companyId)
]);
this.companyService.setCompanies("toggle", companyId);
// trigger state change
this.store.nextAvailableCompanies = this.companyService.nextAvailableCompanies.slice();
browser.clearTimeout(this.store.toggleTimer);
this.store.toggleTimer = browser.setTimeout(() => {
this.companyService.setCompanies("toggle", ...this.store.companiesToToggle);
this.companyService.logNextCompanies();
}, this.constructor.toggleDelay);
}
}
@@ -56,7 +55,7 @@ export class SwitchCompanyMenu extends Component {
setup() {
this.companyService = useService("company");
this.store = useState(store);
this.store.companiesToToggle = [];
this.store.nextAvailableCompanies = [];
}
}
SwitchCompanyMenu.template = "web.SwitchCompanyMenu";
@@ -252,7 +252,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => {
* [x] **Company 3**
*/
await click(scMenuEl.querySelectorAll(".log_into")[1]);
assert.verifySteps(["cids=2%2C3%2C1"]);
assert.verifySteps(["cids=2"]);
});
QUnit.test("multi company mode: log into an already selected company", async (assert) => {
@@ -282,7 +282,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => {
* [x] Company 3 -> log into
*/
await click(scMenuEl.querySelectorAll(".log_into")[2]);
assert.verifySteps(["cids=3%2C2"]);
assert.verifySteps(["cids=3"]);
});
QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => {
@@ -40,7 +40,9 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1, parent_id: false, child_ids: [] },
2: { id: 2, name: "Herman's", sequence: 2, parent_id: false, child_ids: [] },
1: { id: 1, name: "Heroes TM", sequence: 3, parent_id: false, child_ids: [] },
1: { id: 1, name: "Heroes TM", sequence: 3, parent_id: false, child_ids: [4, 5] },
4: { id: 4, name: "Hercules", sequence: 4, parent_id: 1, child_ids: [] },
5: { id: 5, name: "Hulk", sequence: 5, parent_id: 1, child_ids: [] },
},
current_company: 3,
});
@@ -59,10 +61,10 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
assert.strictEqual(target.querySelector("div.o_switch_company_menu").textContent, "Hermit");
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, ".toggle_company", 3);
assert.containsN(target, ".log_into", 3);
assert.containsN(target, ".toggle_company", 5);
assert.containsN(target, ".log_into", 5);
assert.containsOnce(target, ".fa-check-square");
assert.containsN(target, ".fa-square-o", 2);
assert.containsN(target, ".fa-square-o", 4);
assert.strictEqual(
target.querySelector(".fa-check-square").closest(".dropdown-item").textContent,
"Hermit"
@@ -73,7 +75,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
);
assert.strictEqual(
target.querySelector(".dropdown-menu").textContent,
"HermitHerman'sHeroes TM"
"HermitHerman'sHeroes TMHerculesHulk"
);
});
@@ -89,42 +91,46 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [x] **Hermit**
* [ ] Herman's
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 4);
assert.deepEqual(
[...target.querySelectorAll("[data-company-id] .toggle_company")].map(
(el) => el.getAttribute('aria-checked')
),
["true", "false", "false"]
["true", "false", "false", "false", "false"]
);
assert.deepEqual(
[...target.querySelectorAll("[data-company-id] .log_into")].map((el) => el.getAttribute('aria-pressed')),
["true", "false", "false"]
["true", "false", "false", "false", "false"]
);
/**
* [x] **Hermit**
* [x] Herman's -> toggle
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
await click(target.querySelectorAll(".toggle_company")[1]);
assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened");
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 3);
assert.deepEqual(
[...target.querySelectorAll("[data-company-id] .toggle_company")].map(
(el) => el.getAttribute('aria-checked')
),
["true", "true", "false"]
["true", "true", "false", "false", "false"]
);
assert.deepEqual(
[...target.querySelectorAll("[data-company-id] .log_into")].map((el) => el.getAttribute('aria-pressed')),
["true", "false", "false"]
["true", "false", "false", "false", "false"]
);
await prom;
assert.verifySteps(["cids=3%2C2"]);
@@ -144,29 +150,33 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [x] **Hermit**
* [ ] Herman's
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 4);
/**
* [ ] **Hermit** -> toggle all
* [x] Herman's -> toggle all
* [x] Heroes TM -> toggle all
* [ ] Hermit -> toggle all
* [x] **Herman's** -> toggle all
* [x] Heroes TM -> toggle all
* [ ] Hercules
* [ ] Hulk
*/
await click(target.querySelectorAll(".toggle_company")[0]);
await click(target.querySelectorAll(".toggle_company")[1]);
await click(target.querySelectorAll(".toggle_company")[2]);
assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened");
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
assert.containsN(target, "[data-company-id] .fa-check-square", 4);
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
assert.verifySteps([]);
await prom; // await toggle promise
assert.verifySteps(["cids=2%2C1"]);
assert.verifySteps(["cids=2%2C1%2C4%2C5"]);
});
QUnit.test("single company selected: toggling it off will keep it", async (assert) => {
@@ -183,19 +193,23 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [x] **Hermit**
* [ ] Herman's
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 3 });
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 4);
/**
* [ ] **Hermit** -> toggle off
* [x] **Hermit** -> toggle off
* [ ] Herman's
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
await click(target.querySelectorAll(".toggle_company")[0]);
assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 3 });
@@ -203,7 +217,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened");
assert.containsN(target, "[data-company-id] .fa-check-square", 0);
assert.containsN(target, "[data-company-id] .fa-square-o", 3);
assert.containsN(target, "[data-company-id] .fa-square-o", 5);
});
QUnit.test("single company mode: companies can be logged in", async (assert) => {
@@ -218,18 +232,22 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [x] **Hermit**
* [ ] Herman's
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 4);
/**
* [x] **Hermit**
* [ ] Herman's -> log into
* [ ] Hermit
* [x] **Herman's** -> log into
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
await click(target.querySelectorAll(".log_into")[1]);
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
@@ -249,22 +267,26 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [x] Hermit
* [ ] Herman's
* [x] **Heroes TM**
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3, 1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 3);
/**
* [x] Hermit
* [ ] Herman's -> log into
* [x] **Heroes TM**
* [ ] Hermit
* [x] **Herman's** -> log into
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
await click(target.querySelectorAll(".log_into")[1]);
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
assert.verifySteps(["cids=2%2C3%2C1"]);
assert.verifySteps(["cids=2"]);
});
QUnit.test("multi company mode: log into an already selected company", async (assert) => {
@@ -280,22 +302,26 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [ ] Hermit
* [x] **Herman's**
* [x] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2, 1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 3);
/**
* [ ] Hermit
* [x] **Herman's**
* [x] Heroes TM -> log into
* [ ] Herman's
* [x] **Heroes TM** -> log into
* [x] Hercules
* [x] Hulk
*/
await click(target.querySelectorAll(".log_into")[2]);
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
assert.verifySteps(["cids=1%2C2"]);
assert.verifySteps(["cids=1%2C4%2C5"]);
});
QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => {
@@ -310,18 +336,22 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
* [x] **Hermit**
* [ ] Herman's
* [ ] Heroes TM
* [ ] Hercules
* [ ] Hulk
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(target.querySelector(".dropdown-toggle"));
assert.containsN(target, "[data-company-id]", 3);
assert.containsN(target, "[data-company-id]", 5);
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
assert.containsN(target, "[data-company-id] .fa-square-o", 4);
/**
* [ ] **Hermit** -> toggled
* [ ] Herman's -> logged in
* [ ] Hermit -> toggled
* [x] **Herman's** -> logged in
* [ ] Heroes TM -> toggled
* [ ] Hercules
* [ ] Hulk
*/
await click(target.querySelectorAll(".toggle_company")[2]);
await click(target.querySelectorAll(".toggle_company")[0]);