[IMP] web: allow user to select multiple company

Before this commit: when user selects company from company switcher, page is
reloaded, what if user wants to select multiple companies from dropdown, he
have to wait for page reload and need to do same process again.

After this commit: when user selects company from company switcher, page
will not reloaded as soon as user toggles checkbox, user will given time to
select another item or unselect other item from dropdown, so user can select
multiple companies or unselect multiple companies.

task-2467747

closes odoo-dev/odoo#875

Related: odoo-dev/enterprise#148
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Bruno Boi
2021-06-18 21:31:30 +02:00
committed by Aaron Bohy
parent 8fcdcc0b1c
commit 1cec4e0888
7 changed files with 271 additions and 117 deletions
@@ -77,3 +77,18 @@ export function sortBy(array, criterion, order = "asc") {
return order === "asc" ? result : -result;
});
}
/**
* Returns an array containing all the elements of arrayA
* that are not in arrayB and vice-versa.
*
* @param {any[]} arrayA
* @param {any[]} arrayB
* @returns an array containing all the elements of arrayA
* that are not in arrayB and vice-versa.
*/
export function symmetricalDifference(arrayA, arrayB) {
return arrayA
.filter((id) => !arrayB.includes(id))
.concat(arrayB.filter((id) => !arrayA.includes(id)));
}
@@ -2,6 +2,7 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { symmetricalDifference } from "../core/utils/arrays";
function parseCompanyIds(cidsFromHash) {
const cids = [];
@@ -53,32 +54,29 @@ export const companyService = {
get currentCompany() {
return availableCompanies[allowedCompanyIds[0]];
},
setCompanies(mode, companyId) {
setCompanies(mode, ...companyIds) {
// compute next company ids
let nextCompanyIds = allowedCompanyIds.slice();
let nextCompanyIds;
if (mode === "toggle") {
if (nextCompanyIds.includes(companyId)) {
nextCompanyIds = nextCompanyIds.filter((id) => id !== companyId);
} else {
nextCompanyIds.push(companyId);
}
nextCompanyIds = symmetricalDifference(allowedCompanyIds, companyIds);
} else if (mode === "loginto") {
if (nextCompanyIds.length === 1) {
const companyId = companyIds[0];
if (allowedCompanyIds.length === 1) {
// 1 enabled company: stay in single company mode
nextCompanyIds = [companyId];
} else {
// multi company mode
if (nextCompanyIds.includes(companyId)) {
nextCompanyIds = nextCompanyIds.filter((id) => id !== companyId);
}
nextCompanyIds.unshift(companyId);
nextCompanyIds = [
companyId,
...allowedCompanyIds.filter((id) => id !== companyId),
];
}
}
allowedCompanyIds = nextCompanyIds.length ? nextCompanyIds : [companyId];
nextCompanyIds = nextCompanyIds.length ? nextCompanyIds : [companyIds[0]];
// apply them
router.pushState({ cids: allowedCompanyIds }, { lock: true });
cookie.setCookie("cids", allowedCompanyIds);
router.pushState({ cids: nextCompanyIds }, { lock: true });
cookie.setCookie("cids", nextCompanyIds);
browser.setTimeout(() => browser.location.reload()); // history.pushState is a little async
},
};
@@ -2,8 +2,13 @@
import { useService } from "../../core/service_hook";
import { registry } from "../../core/registry";
import { browser } from "../../core/browser/browser";
import { symmetricalDifference } from "../../core/utils/arrays";
export class SwitchCompanyMenu extends owl.Component {
const { Component, hooks } = owl;
const { useState } = hooks;
export class SwitchCompanyMenu extends Component {
static isDisplayed(env) {
const { availableCompanies } = env.services.company;
return Object.keys(availableCompanies).length > 1 && !env.isSmall;
@@ -12,16 +17,32 @@ export class SwitchCompanyMenu extends owl.Component {
setup() {
this.companyService = useService("company");
this.currentCompany = this.companyService.currentCompany;
this.state = useState({ companiesToToggle: [] });
}
toggleCompany(companyId) {
this.companyService.setCompanies("toggle", companyId);
this.state.companiesToToggle = symmetricalDifference(this.state.companiesToToggle, [
companyId,
]);
browser.clearTimeout(this.toggleTimer);
this.toggleTimer = browser.setTimeout(() => {
this.companyService.setCompanies("toggle", ...this.state.companiesToToggle);
}, this.constructor.toggleDelay);
}
logIntoCompany(companyId) {
browser.clearTimeout(this.toggleTimer);
this.companyService.setCompanies("loginto", companyId);
}
get selectedCompanies() {
return symmetricalDifference(
this.companyService.allowedCompanyIds,
this.state.companiesToToggle
);
}
}
SwitchCompanyMenu.template = "web.SwitchCompanyMenu";
SwitchCompanyMenu.toggleDelay = 1000;
registry.category("systray").add("SwitchCompanyMenu", SwitchCompanyMenu, { sequence: 1 });
@@ -19,7 +19,7 @@
<t t-name="web.SwitchCompanyItem" owl="1">
<div>
<t t-set="isCompanySelected" t-value="companyService.allowedCompanyIds.includes(company.id)"/>
<t t-set="isCompanySelected" t-value="selectedCompanies.includes(company.id)"/>
<t t-set="isCurrent" t-value="company.id === companyService.currentCompany.id"/>
<DropdownItem>
<div class="dropdown-item d-flex py-0 px-0" data-menu="company" t-att-data-company-id="company.id">
@@ -29,7 +29,7 @@
t-att-aria-label="company.name"
tabindex="0"
class="ml-auto pl-3 pr-3 border border-top-0 border-left-0 border-bottom-0 toggle_company o_py"
t-on-click="toggleCompany(company.id)">
t-on-click.stop="toggleCompany(company.id)">
<span style="height: 2rem;">
<i class="fa fa-fw pt-2" t-attf-class="{{isCompanySelected ? 'fa-check-square' : 'fa-square-o'}}"/>
@@ -46,7 +46,7 @@
t-attf-style="{{isCurrent ? 'background-color: lightgrey;' : ''}}"
t-on-click="logIntoCompany(company.id)">
<span class='mr-3 company_label' t-attf-class="{{isCompanySelected ? '' : 'text-muted'}}">
<span class='mr-3 company_label' t-attf-class="{{selectedCompanies.includes(company.id) ? '' : 'text-muted'}}">
<t t-esc="company.name"/>
</span>
@@ -152,8 +152,9 @@ function makeMockLocation() {
* @param {Object} [params={}]
* @param {Object} [params.initialRoute] initial route object
* @param {Object} [params.onPushState] hook on the "pushState" method
* @param {Object} [params.onReplaceState] hook on the "onReplaceState" method
* @param {Object} [params.onRedirect] hook on the "redirect" method
* @returns {RouterService}
* @returns {typeof routerService}
*/
export function makeFakeRouterService(params = {}) {
const mockLocation = makeMockLocation();
@@ -231,14 +232,6 @@ export function makeFakeUIService(values = {}) {
};
}
export function makeFakeCompanyService() {
const mockLocation = makeMockLocation();
patchWithCleanup(browser, {
location: mockLocation,
});
return companyService;
}
export const fakeCookieService = {
start() {
const cookie = {};
@@ -297,7 +290,7 @@ export function makeFakeNotificationService(createMock, closeMock) {
}
export const mocks = {
company: makeFakeCompanyService,
company: () => companyService,
cookie: () => fakeCookieService,
effect: () => effectService, // BOI The real service ? Is this what we want ?
localization: makeFakeLocalizationService,
+4 -4
View File
@@ -60,15 +60,15 @@ export async function nextTick() {
}
export function makeDeferred() {
/** @type {(value:any)=>void} */
let resolve;
/** @type {(reason?:any)=>void} */
let reject;
let prom = new Promise((res, rej) => {
const prom = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
prom.resolve = resolve;
prom.reject = reject;
return prom;
return Object.assign(prom, { resolve, reject });
}
function findElement(el, selector) {
@@ -5,12 +5,25 @@ import { hotkeyService } from "@web/webclient/hotkeys/hotkey_service";
import { SwitchCompanyMenu } from "@web/webclient/switch_company_menu/switch_company_menu";
import { registerCleanup } from "../helpers/cleanup";
import { makeTestEnv } from "../helpers/mock_env";
import { makeFakeCompanyService, makeFakeUIService } from "../helpers/mock_services";
import { click, getFixture, patchWithCleanup } from "../helpers/utils";
import { companyService } from "@web/webclient/company_service";
import { makeFakeRouterService, makeFakeUIService } from "../helpers/mock_services";
import { click, getFixture, makeDeferred, patchWithCleanup } from "../helpers/utils";
const { mount } = owl;
const serviceRegistry = registry.category("services");
async function createSwitchCompanyMenu(routerParams = {}, toggleDelay = 0) {
patchWithCleanup(SwitchCompanyMenu, { toggleDelay });
if (routerParams.onPushState || routerParams.initialRoute) {
serviceRegistry.add("router", makeFakeRouterService(routerParams));
}
const env = await makeTestEnv();
const target = getFixture();
const scMenu = await mount(SwitchCompanyMenu, { env, target });
registerCleanup(() => scMenu.destroy());
return scMenu;
}
QUnit.module("SwitchCompanyMenu", (hooks) => {
hooks.beforeEach(() => {
patchWithCleanup(odoo.session_info.user_companies, {
@@ -22,17 +35,14 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
current_company: 1,
});
serviceRegistry.add("ui", makeFakeUIService());
serviceRegistry.add("company", makeFakeCompanyService());
serviceRegistry.add("company", companyService);
serviceRegistry.add("hotkey", hotkeyService);
});
QUnit.test("basic rendering", async (assert) => {
assert.expect(10);
const env = await makeTestEnv();
const target = getFixture();
const scMenu = await mount(SwitchCompanyMenu, { env, target });
registerCleanup(() => scMenu.destroy());
const scMenu = await createSwitchCompanyMenu();
assert.strictEqual(scMenu.el.tagName.toUpperCase(), "DIV");
assert.hasClass(scMenu.el, "o_switch_company_menu");
@@ -57,12 +67,15 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
);
});
QUnit.test("companies can be toggled and logged in", async (assert) => {
assert.expect(22);
const env = await makeTestEnv();
const target = getFixture();
const scMenu = await mount(SwitchCompanyMenu, { env, target });
registerCleanup(() => scMenu.destroy());
QUnit.test("companies can be toggled: toggle a second company", async (assert) => {
assert.expect(10);
const prom = makeDeferred();
function onPushState(url) {
assert.step(url.split("#")[1]);
prom.resolve();
}
const scMenu = await createSwitchCompanyMenu({ onPushState });
/**
* [x] **Company 1**
@@ -71,105 +84,219 @@ QUnit.module("SwitchCompanyMenu", (hooks) => {
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".toggle_company")[1]);
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 1);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 2);
/**
* [x] **Company 1**
* [x] Company 2 -> toggle
* [ ] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1, 2]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".toggle_company")[1]);
assert.containsOnce(scMenu.el, ".o_dropdown_menu", "dropdown is still opened");
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 2);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 1);
await prom;
assert.verifySteps(["cids=1%2C2"]);
});
QUnit.test("can toggle multiple companies at once", async (assert) => {
assert.expect(11);
const prom = makeDeferred();
function onPushState(url) {
assert.step(url.split("#")[1]);
prom.resolve();
}
const scMenu = await createSwitchCompanyMenu({ onPushState }, 50);
/**
* [x] **Company 1**
* [ ] Company 2 -> toggle
* [ ] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".toggle_company")[0]);
/**
* [x] **Company 1** -> tried to toggle
* [ ] Company 2
* [ ] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".log_into")[1]);
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 1);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 2);
/**
* [ ] Company 1
* [x] **Company 2** -> click label (log into)
* [ ] **Company 1** -> toggle all
* [x] Company 2 -> toggle all
* [x] Company 3 -> toggle all
*/
await click(scMenu.el.querySelectorAll(".toggle_company")[0]);
await click(scMenu.el.querySelectorAll(".toggle_company")[1]);
await click(scMenu.el.querySelectorAll(".toggle_company")[2]);
assert.containsOnce(scMenu.el, ".o_dropdown_menu", "dropdown is still opened");
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 2);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 1);
assert.verifySteps([]);
await prom; // await toggle promise
assert.verifySteps(["cids=2%2C3"]);
});
QUnit.test("single company selected: toggling it off will keep it", async (assert) => {
assert.expect(10);
const prom = makeDeferred();
function onPushState(url) {
assert.step(url.split("#")[1]);
prom.resolve();
}
const scMenu = await createSwitchCompanyMenu({ onPushState });
/**
* [x] **Company 1**
* [ ] Company 2
* [ ] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".toggle_company")[2]);
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 1);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 2);
/**
* [ ] **Company 1** -> toggle off
* [ ] Company 2
* [ ] Company 3
*/
await click(scMenu.el.querySelectorAll(".toggle_company")[0]);
assert.containsOnce(scMenu.el, ".o_dropdown_menu", "dropdown is still opened");
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 0);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 3);
await prom;
assert.verifySteps(["cids=1"]);
});
QUnit.test("single company mode: companies can be logged in", async (assert) => {
assert.expect(8);
function onPushState(url) {
assert.step(url.split("#")[1]);
}
const scMenu = await createSwitchCompanyMenu({ onPushState });
/**
* [x] **Company 1**
* [ ] Company 2
* [ ] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 1);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 2);
/**
* [x] **Company 1**
* [ ] Company 2 -> log into
* [ ] Company 3
*/
await click(scMenu.el.querySelectorAll(".log_into")[1]);
assert.containsNone(scMenu.el, ".o_dropdown_menu", "dropdown is directly closed");
assert.verifySteps(["cids=2"]);
});
QUnit.test("multi company mode: log into a non selected company", async (assert) => {
assert.expect(8);
function onPushState(url) {
assert.step(url.split("#")[1]);
}
const initialRoute = { hash: "cids=3%2C1" };
const scMenu = await createSwitchCompanyMenu({ onPushState, initialRoute });
/**
* [x] Company 1
* [ ] Company 2
* [x] **Company 3**
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3, 1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 2);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 1);
/**
* [x] Company 1
* [ ] Company 2 -> log into
* [x] **Company 3**
*/
await click(scMenu.el.querySelectorAll(".log_into")[1]);
assert.containsNone(scMenu.el, ".o_dropdown_menu", "dropdown is directly closed");
assert.verifySteps(["cids=2%2C3%2C1"]);
});
QUnit.test("multi company mode: log into an already selected company", async (assert) => {
assert.expect(8);
function onPushState(url) {
assert.step(url.split("#")[1]);
}
const initialRoute = { hash: "cids=2%2C3" };
const scMenu = await createSwitchCompanyMenu({ onPushState, initialRoute });
/**
* [ ] Company 1
* [x] **Company 2**
* [x] Company 3 -> toggle
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2, 3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".toggle_company")[0]);
/**
* [x] Company 1 -> toggle
* [x] **Company 2**
* [x] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2, 3, 1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".log_into")[0]);
/**
* [x] **Company 1** -> click label
* [x] Company 2
* [x] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1, 2, 3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".log_into")[0]);
/**
* [x] **Company 1** -> tried to click label
* [x] Company 2
* [x] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1, 2, 3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".toggle_company")[0]);
/**
* [ ] Company 1 -> toggle
* [x] **Company 2**
* [x] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2, 3]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
await click(scMenu.el.querySelectorAll(".log_into")[0]);
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 2);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 1);
/**
* [x] **Company 1** -> click label
* [x] Company 2
* [x] Company 3
* [ ] Company 1
* [x] **Company 2**
* [x] Company 3 -> log into
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1, 2, 3]);
await click(scMenu.el.querySelectorAll(".log_into")[2]);
assert.containsNone(scMenu.el, ".o_dropdown_menu", "dropdown is directly closed");
assert.verifySteps(["cids=3%2C2"]);
});
QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => {
assert.expect(8);
function onPushState(url) {
assert.step(url.split("#")[1]);
}
const scMenu = await createSwitchCompanyMenu({ onPushState }, 50);
/**
* [x] **Company 1**
* [ ] Company 2
* [ ] Company 3
*/
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
await click(scMenu.el.querySelector(".o_dropdown_toggler"));
assert.containsN(scMenu.el, "[data-company-id]", 3);
assert.containsN(scMenu.el, "[data-company-id] .fa-check-square", 1);
assert.containsN(scMenu.el, "[data-company-id] .fa-square-o", 2);
/**
* [ ] **Company 1** -> toggled
* [ ] Company 2 -> logged in
* [ ] Company 3 -> toggled
*/
await click(scMenu.el.querySelectorAll(".toggle_company")[2]);
await click(scMenu.el.querySelectorAll(".toggle_company")[0]);
await click(scMenu.el.querySelectorAll(".log_into")[1]);
assert.containsNone(scMenu.el, ".o_dropdown_menu", "dropdown is directly closed");
assert.verifySteps(["cids=2"]);
});
});