import { defineDocumentSpreadsheetModels, defineDocumentSpreadsheetTestAction, getBasicData, getBasicServerData, getDocumentBasicData, } from "@documents_spreadsheet/../tests/helpers/data"; import { createSpreadsheetFromListView, invokeInsertListInSpreadsheetDialog, spawnListViewForSpreadsheet, } from "@documents_spreadsheet/../tests/helpers/list_helpers"; import { createSpreadsheet } from "@documents_spreadsheet/../tests/helpers/spreadsheet_test_utils"; import { getHighlightsFromStore } from "@documents_spreadsheet/../tests/helpers/store_helpers"; import { SpreadsheetAction } from "@documents_spreadsheet/bundle/actions/spreadsheet_action"; import { describe, expect, getFixture, test } from "@odoo/hoot"; import { hover, leave } from "@odoo/hoot-dom"; import { animationFrame, mockDate } from "@odoo/hoot-mock"; import * as spreadsheet from "@odoo/o-spreadsheet"; import { onMounted } from "@odoo/owl"; import { addGlobalFilter, selectCell, setCellContent, } from "@spreadsheet/../tests/helpers/commands"; import { Partner, Product, ResUsers } from "@spreadsheet/../tests/helpers/data"; import { getCellFormula, getCellValue, getEvaluatedCell, } from "@spreadsheet/../tests/helpers/getters"; import { insertListInSpreadsheet } from "@spreadsheet/../tests/helpers/list"; import { getZoneOfInsertedDataSource } from "@spreadsheet/../tests/helpers/pivot"; import { doMenuAction, getActionMenu } from "@spreadsheet/../tests/helpers/ui"; import { waitForDataLoaded } from "@spreadsheet/helpers/model"; import { getSpreadsheetActionModel } from "@spreadsheet_edition/../tests/helpers/webclient_helpers"; import { InsertListSpreadsheetMenu } from "@spreadsheet_edition/assets/list_view/insert_list_spreadsheet_menu_owl"; import { SpreadsheetSelectorDialog } from "@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog"; import { insertList } from "@spreadsheet_edition/bundle/list/list_init_callback"; import * as dsHelpers from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers"; import { contains, fields, mountView, onRpc, pagerNext, patchWithCleanup, toggleActionMenu, makeServerError, serverState, mockService, } from "@web/../tests/web_test_helpers"; import { registry } from "@web/core/registry"; import { user } from "@web/core/user"; import { Deferred } from "@web/core/utils/concurrency"; import { mockActionService } from "../helpers/spreadsheet_test_utils"; defineDocumentSpreadsheetModels(); defineDocumentSpreadsheetTestAction(); describe.current.tags("desktop"); const { topbarMenuRegistry, cellMenuRegistry } = spreadsheet.registries; const { toZone } = spreadsheet.helpers; test("insert list formulas", async () => { const { model } = await createSpreadsheetFromListView({ linesNumber: 3, serverData: { models: getBasicData(), views: { "partner,false,list": ` `, }, }, }); expect(model.getters.getListDefinition("1").columns).toEqual(["foo", "bar"]); expect(getCellFormula(model, "A1")).toBe('=ODOO.LIST.HEADER(1,"foo","Foo")'); expect(getCellFormula(model, "B1")).toBe('=ODOO.LIST.HEADER(1,"bar","Bar")'); expect(getCellFormula(model, "A2")).toBe('=ODOO.LIST(1,1,"foo")'); expect(getCellFormula(model, "A3")).toBe('=ODOO.LIST(1,2,"foo")'); expect(getCellFormula(model, "A4")).toBe('=ODOO.LIST(1,3,"foo")'); expect(getCellFormula(model, "A5")).toBe(""); expect(getCellFormula(model, "B2")).toBe('=ODOO.LIST(1,1,"bar")'); expect(getCellFormula(model, "B3")).toBe('=ODOO.LIST(1,2,"bar")'); expect(getCellFormula(model, "B4")).toBe('=ODOO.LIST(1,3,"bar")'); expect(getCellFormula(model, "B5")).toBe(""); }); test("List export with a invisible field", async () => { const { model } = await createSpreadsheetFromListView({ serverData: { models: getBasicData(), views: { "partner,false,list": ` `, }, }, }); expect(model.getters.getListDefinition("1").columns).toEqual(["bar"]); }); test("List export with a widget handle", async () => { const { model } = await createSpreadsheetFromListView({ serverData: { models: getBasicData(), views: { "partner,false,list": ` `, }, }, }); expect(model.getters.getListDefinition("1").columns).toEqual(["bar"]); }); test("Lists are inserted with a table in the webclient", async () => { const { model } = await createSpreadsheetFromListView(); const sheetId = model.getters.getActiveSheetId(); const table = model.getters.getTable({ sheetId, col: 2, row: 2 }); expect(table.range.zone).toEqual(toZone("A1:D11")); }); test("property fields are not exported", async () => { const data = getBasicData(); const propertyDefinition = { type: "char", name: "property_char", string: "Property char", }; const product = Product._records[0]; product.properties_definitions = [propertyDefinition]; data.partner.records = [ { id: 1, bar: true, product_id: product.id, partner_properties: { [propertyDefinition.name]: "CHAR", }, }, ]; const propertyField = fields.Properties({ string: "Property char", definition_record: "product_id", definition_record_field: "properties_definitions", }); Partner._fields.partner_properties = propertyField; const { model } = await createSpreadsheetFromListView({ actions: async () => { // display the property which is an optional column await contains(".o_optional_columns_dropdown_toggle").click(); await contains(".o-dropdown--menu input[type='checkbox']").click(); expect(".o_list_renderer th[data-name='partner_properties.property_char']").toHaveCount( 1 ); expect.step("display_property"); }, serverData: { models: data, views: { "partner,false,list": /*xml*/ ` `, }, }, }); expect(model.getters.getListDefinition("1").columns).toEqual(["product_id", "bar"]); expect.verifySteps(["display_property"]); }); test("json fields are not exported", async () => { const { model } = await createSpreadsheetFromListView({ serverData: { models: getBasicData(), views: { "partner,false,list": ` `, }, }, }); expect(model.getters.getListDefinition("1").columns).toEqual(["bar"]); }); test("list side panel is open at insertion", async function () { await createSpreadsheetFromListView(); expect(".o-listing-details-side-panel").toHaveCount(1); }); test("An error is displayed in the side panel if the list has invalid model", async function () { const { model, env } = await createSpreadsheetFromListView({ mockRPC: async function (route, { model, method, kwargs }) { if (model === "unknown" && method === "fields_get") { throw makeServerError({ code: 404 }); } }, }); await contains(".o-sidePanelClose").click(); const listId = model.getters.getListIds()[0]; const listDefinition = model.getters.getListModelDefinition(listId); model.dispatch("UPDATE_ODOO_LIST", { listId, list: { ...listDefinition, metaData: { ...listDefinition.metaData, resModel: "unknown", }, }, }); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); expect(".o-validation-error").toHaveCount(1); }); test("Open list properties", async function () { const { env } = await createSpreadsheetFromListView(); await doMenuAction(topbarMenuRegistry, ["data", "item_list_1"], env); await animationFrame(); const target = getFixture(); const title = target.querySelector(".o-sidePanelTitle").innerText; expect(title).toBe("List #1"); const sections = target.querySelectorAll(".o-section"); expect(sections.length).toBe(6, { message: "it should have 6 sections" }); const [listName, listModel, columns, domain] = sections; expect(listName.children[0]).toHaveText("List Name"); expect(listName.children[1].querySelector("input").value).toBe("Partners"); expect(listModel.children[0]).toHaveText("Model"); expect(listModel.children[1]).toHaveText("Partner (partner)"); expect(columns.children[0]).toHaveText("Columns"); expect(domain.children[0]).toHaveText("Domain"); expect(domain.children[1]).toHaveText("Match all records\nInclude archived"); }); test("A warning is displayed in the menu item if the list is unused", async function () { const { model } = await createSpreadsheetFromListView(); model.dispatch("CREATE_SHEET", { sheetId: "sh2", name: "Sheet2" }); insertListInSpreadsheet(model, { sheetId: "sh2", model: "product", columns: ["name", "active"], }); const target = getFixture(); await contains("div[data-id='data']").click(); const menuItemList1 = target.querySelector("div[data-name='item_list_1']"); const menuItemList2 = target.querySelector("div[data-name='item_list_2']"); expect(menuItemList1.querySelector(".o-unused-list-icon")).toEqual(null); expect(menuItemList2.querySelector(".o-unused-list-icon")).toEqual(null); model.dispatch("DELETE_SHEET", { sheetId: "sh2" }); await animationFrame(); expect(menuItemList1.querySelector(".o-unused-list-icon")).toEqual(null); expect(menuItemList2.querySelector(".o-unused-list-icon")).toHaveCount(1); }); test("A warning is displayed in the side panel if the list is unused", async function () { const { model, env } = await createSpreadsheetFromListView(); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); expect(".o-validation-warning").toHaveCount(0); model.dispatch("CREATE_SHEET", { sheetId: "sh2", name: "Sheet2" }); const activeSheetId = model.getters.getActiveSheetId(); model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: "sh2" }); model.dispatch("DELETE_SHEET", { sheetId: activeSheetId }); await animationFrame(); expect(".o-validation-warning").toHaveCount(1); model.dispatch("REQUEST_UNDO"); await animationFrame(); expect(".o-validation-warning").toHaveCount(0); }); test("Deleting the list closes the side panel", async function () { const { model, env } = await createSpreadsheetFromListView(); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); const titleSelector = ".o-sidePanelTitle"; expect(titleSelector).toHaveText("List #1"); model.dispatch("REMOVE_ODOO_LIST", { listId }); await animationFrame(); expect(titleSelector).toHaveCount(0); }); test("Undo a list insertion closes the side panel", async function () { const { model, env } = await createSpreadsheetFromListView(); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); const titleSelector = ".o-sidePanelTitle"; expect(titleSelector).toHaveText("List #1"); model.dispatch("REQUEST_UNDO"); model.dispatch("REQUEST_UNDO"); await animationFrame(); expect(titleSelector).toHaveCount(0); }); test("Add list in an existing spreadsheet", async () => { const { model, env } = await createSpreadsheetFromListView(); const list = model.getters.getListDefinition("1"); const fields = model.getters.getListDataSource("1").getFields(); const callback = insertList.bind({ isEmptySpreadsheet: false })({ list: { ...list, columns: list.columns.map((col) => ({ name: col })), }, threshold: 10, fields: fields, name: "my list", }); model.dispatch("CREATE_SHEET", { sheetId: "42", position: 1 }); const activeSheetId = model.getters.getActiveSheetId(); expect(model.getters.getSheetIds()).toEqual([activeSheetId, "42"]); await callback(model, env.__spreadsheet_stores__); expect(model.getters.getSheetIds().length).toBe(3); expect(model.getters.getSheetIds()[0]).toEqual(activeSheetId); expect(model.getters.getSheetIds()[1]).toBe("42"); expect(".o-listing-details-side-panel").toHaveCount(1); }); test("Verify absence of list properties on non-list cell", async function () { const { model, env } = await createSpreadsheetFromListView(); selectCell(model, "Z26"); const root = cellMenuRegistry.getAll().find((item) => item.id === "listing_properties"); expect(root.isVisible(env)).toBe(false); }); test("Verify absence of list properties on formula with invalid list Id", async function () { const { model, env } = await createSpreadsheetFromListView(); setCellContent(model, "A1", `=ODOO.LIST.HEADER("fakeId", "foo")`); const root = cellMenuRegistry.getAll().find((item) => item.id === "listing_properties"); expect(root.isVisible(env)).toBe(false); setCellContent(model, "A1", `=ODOO.LIST("fakeId", "2", "bar")`); expect(root.isVisible(env)).toBe(false); }); test("Re-insert a list correctly ask for lines number", async function () { const { model, env } = await createSpreadsheetFromListView(); selectCell(model, "Z26"); await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env); await animationFrame(); expect(".modal-body input").toHaveCount(1); expect(".modal-body input").toHaveProperty("type", "number"); await contains(".o_dialog .btn-secondary").click(); // cancel expect(getCellFormula(model, "Z26")).toBe("", { message: "the list is not re-inserted" }); await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env); await animationFrame(); await contains(".o_dialog .btn-primary").click(); // confirm expect(getCellFormula(model, "Z26")).toBe('=ODOO.LIST.HEADER(1,"foo","Foo")', { message: "the list is re-inserted", }); }); test("Validates input and shows error message when input is invalid", async function () { const { model, env } = await createSpreadsheetFromListView(); selectCell(model, "Z1"); await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env); await animationFrame(); await contains(".modal-body input").edit(""); await contains(".modal-content > .modal-footer > .btn-primary").click(); expect(".modal-body span.text-danger:only").toHaveText("Please enter a valid number."); }); test("Re-insert a list with a selected number of records", async function () { const { model, env } = await createSpreadsheetFromListView(); selectCell(model, "Z1"); await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env); await animationFrame(); await contains(".modal-body input").edit("2000"); await contains(".modal-content > .modal-footer > .btn-primary").click(); expect(model.getters.getNumberRows(model.getters.getActiveSheetId())).toBe(2001); }); test("Re-insert a list also applies a table", async function () { const { model, env } = await createSpreadsheetFromListView(); const sheetId = model.getters.getActiveSheetId(); let table = model.getters.getTable({ sheetId, col: 0, row: 49 }); expect(table).toBe(undefined); selectCell(model, "A50"); await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env); await animationFrame(); await contains(".modal-body input").edit("10"); await contains(".modal-content > .modal-footer > .btn-primary").click(); table = model.getters.getTable({ sheetId, col: 0, row: 49 }); expect(table.range.zone).toEqual(toZone("A50:D60")); }); test("user related context is not saved in the spreadsheet", async function () { ResUsers._records = getDocumentBasicData().models["res.users"].records; registry.category("favoriteMenu").add( "insert-in-spreadsheet-menu", { Component: InsertListSpreadsheetMenu, groupNumber: 4, }, { sequence: 5 } ); patchWithCleanup(SpreadsheetSelectorDialog.prototype, { setup() { super.setup(); expect(this.props.actionOptions.preProcessingAsyncActionData.list.context).toEqual( { default_stage_id: 5, }, { message: "user related context is not stored in context" } ); }, }); serverState.companies = [{ id: 15, name: "Hermit" }]; const userCtx = user.context; patchWithCleanup(user, { get context() { return Object.assign({}, userCtx, { allowed_company_ids: [15], tz: "bx", lang: "FR", uid: 4, }); }, }); patchWithCleanup(user, { userId: 4 }); const context = { ...user.context, default_stage_id: 5, }; const { env } = await mountView({ type: "list", resModel: "partner", context, arch: ` `, config: { actionType: "ir.actions.act_window", getDisplayName: () => "Test", viewType: "list", }, }); await invokeInsertListInSpreadsheetDialog(env); await contains(".modal button.btn-primary").click(); }); test("Selected records from current page are inserted correctly", async function () { const def = new Deferred(); let spreadsheetAction = {}; patchWithCleanup(SpreadsheetAction.prototype, { setup() { super.setup(); onMounted(() => { spreadsheetAction = this; def.resolve(); }); }, }); const serverData = { models: getBasicData(), views: { "partner,false,list": ` `, }, }; await spawnListViewForSpreadsheet({ serverData, }); /** Insert the selected records from current page in a new spreadsheet */ const target = getFixture(); await contains(target.querySelectorAll("td.o_list_record_selector input")[1]).click(); await pagerNext(); await contains(target.querySelectorAll("td.o_list_record_selector input")[0]).click(); await toggleActionMenu(); const insertMenuItem = [...target.querySelectorAll(".o-dropdown--menu .o_menu_item")].filter( (el) => el.innerText === "Insert in spreadsheet" )[0]; await contains(insertMenuItem).click(); await contains(".modal-content > .modal-footer > .btn-primary").click(); await def; const model = getSpreadsheetActionModel(spreadsheetAction); await waitForDataLoaded(model); expect(getCellValue(model, "A2")).toBe(17, { message: "First record from page 2 (i.e. 3 of 4 records) should be inserted", }); expect(getCellValue(model, "A3")).toBe(null); }); test("Selected all records from current page are inserted correctly", async function () { const def = new Deferred(); let spreadsheetAction = {}; patchWithCleanup(SpreadsheetAction.prototype, { setup() { super.setup(); onMounted(() => { spreadsheetAction = this; def.resolve(); }); }, }); const serverData = { models: getBasicData(), views: { "partner,false,list": ` `, }, }; await spawnListViewForSpreadsheet({ serverData, }); /** Insert the selected records from current page in a new spreadsheet */ const target = getFixture(); await contains(target.querySelectorAll("td.o_list_record_selector input")[1]).click(); await contains(target.querySelectorAll("td.o_list_record_selector input")[0]).click(); await contains(".o_select_domain").click(); await toggleActionMenu(); const insertMenuItem = [...target.querySelectorAll(".o-dropdown--menu .o_menu_item")].filter( (el) => el.innerText === "Insert in spreadsheet" )[0]; await contains(insertMenuItem).click(); await contains(".modal-content > .modal-footer > .btn-primary").click(); await def; const model = getSpreadsheetActionModel(spreadsheetAction); await waitForDataLoaded(model); expect(getCellValue(model, "A2")).toBe(12, { message: "First record from page 2 (i.e. 3 of 4 records) should be inserted", }); expect(getCellValue(model, "A3")).toBe(1); expect(getCellValue(model, "A4")).toBe(17); expect(getCellValue(model, "A5")).toBe(2); }); test("Insert in spreadsheet is avaiblable on a list grouped by m2m field", async function () { let spreadsheetAction; patchWithCleanup(SpreadsheetAction.prototype, { setup() { super.setup(); spreadsheetAction = this; }, }); const serverData = { models: getBasicData(), views: { "partner,false,list": ` `, }, }; await spawnListViewForSpreadsheet({ serverData, groupBy: ["tag_ids"], }); const target = getFixture(); await contains(target.querySelectorAll(".o_list_record_selector input")[0]).click(); await toggleActionMenu(); await contains(".o-dropdown--menu .o_menu_item:has(.oi-view-list)").click(); await contains(".modal button.btn-primary").click(); await animationFrame(); const model = getSpreadsheetActionModel(spreadsheetAction); await waitForDataLoaded(model); expect(getCellValue(model, "A2")).toBe(12); expect(getCellValue(model, "A3")).toBe(1); expect(getCellValue(model, "A4")).toBe(17); expect(getCellValue(model, "A5")).toBe(2); }); test("Can see record of a list", async function () { const { webClient, model } = await createSpreadsheetFromListView(); const listId = model.getters.getListIds()[0]; const dataSource = model.getters.getListDataSource(listId); const env = { ...webClient.env, model, services: { ...model.config.custom.env.services, action: { doAction: (params) => { expect.step(params.res_model); expect.step(params.res_id.toString()); }, }, }, }; selectCell(model, "A2"); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); await root.execute(env); expect.verifySteps(["partner", dataSource.getIdFromPosition(0).toString()]); selectCell(model, "A3"); await root.execute(env); expect.verifySteps(["partner", dataSource.getIdFromPosition(1).toString()]); // From a cell inside a merge model.dispatch("ADD_MERGE", { sheetId: model.getters.getActiveSheetId(), target: [toZone("A3:B3")], force: true, // there are data in B3 }); selectCell(model, "B3"); await root.execute(env); expect.verifySteps(["partner", dataSource.getIdFromPosition(1).toString()]); }); test("See record of list is only displayed on list formula with only one list formula", async function () { const { webClient, model } = await createSpreadsheetFromListView(); const env = { ...webClient.env, model, services: model.config.custom.env.services, }; setCellContent(model, "A1", "test"); setCellContent(model, "A2", `=ODOO.LIST("1","1","foo")`); setCellContent(model, "A3", `=ODOO.LIST("1","1","foo")+LIST("1","1","foo")`); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); selectCell(model, "A1"); expect(root.isVisible(env)).toBe(false); selectCell(model, "A2"); expect(root.isVisible(env)).toBe(true); selectCell(model, "A3"); expect(root.isVisible(env)).toBe(false); }); test("See records is visible even if the formula is lowercase", async function () { const { env, model } = await createSpreadsheetFromListView(); selectCell(model, "B2"); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); expect(root.isVisible(env)).toBe(true); setCellContent(model, "B2", getCellFormula(model, "B2").replace("ODOO.LIST", "odoo.list")); expect(root.isVisible(env)).toBe(true); }); test("See records is not visible if the formula is in error", async function () { const { env, model } = await createSpreadsheetFromListView(); selectCell(model, "B2"); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); expect(root.isVisible(env)).toBe(true); setCellContent( model, "B2", getCellFormula(model, "B2").replace(`ODOO.LIST(1`, `ODOO.LIST("5)`) ); //Invalid id expect(getEvaluatedCell(model, "B2").message).not.toBe(undefined); expect(root.isVisible(env)).toBe(false); }); test("See record.isVisible() don't throw on spread values", async function () { const { env, model } = await createSpreadsheet(); setCellContent(model, "A1", "A1"); setCellContent(model, "A2", "A2"); setCellContent(model, "C1", "=TRANSPOSE(A1:A2)"); selectCell(model, "D1"); await animationFrame(); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); expect(root.isVisible(env)).toBe(false); }); test("Cannot see record of list formula without value", async function () { const { env, model } = await createSpreadsheetFromListView(); expect(getCellFormula(model, "A6")).toBe(`=ODOO.LIST(1,5,"foo")`); expect(getCellValue(model, "A6")).toBe("", { message: "A6 is empty" }); selectCell(model, "A6"); const action = await getActionMenu(cellMenuRegistry, ["list_see_record"], env); expect(action.isVisible(env)).toBe(false); }); test("'See records' loads a specific action if set in the list definition", async function () { const actionXmlId = "spreadsheet.partner_action"; const { webClient, model } = await createSpreadsheetFromListView({ actionXmlId }); const actionService = webClient.env.services.action; const env = { ...webClient.env, model, services: { ...model.config.custom.env.services, action: { ...actionService, doAction: (params) => { expect(params.id).not.toBe(undefined); expect(params.xml_id).not.toBe(undefined); expect.step(params.res_model); expect.step(params.res_id.toString()); }, }, }, }; selectCell(model, "C3"); await animationFrame(); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); await root.execute(env); expect.verifySteps(["partner", "2"]); }); test("Context is passed correctly to the action service", async function () { const serverData = getBasicServerData(); const actionXmlId = "spreadsheet.partner_action"; serverData.views["partner,false,search"] = /* xml */ ` `; const { model, env } = await createSpreadsheetFromListView({ serverData, actionXmlId, additionalContext: { search_default_filter: 1 }, }); mockActionService((action) => { expect.step("loadAction"); expect(action.context).toEqual({ search_default_filter: 1 }); }); selectCell(model, "A2"); await animationFrame(); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); await root.execute(env); expect.verifySteps(["loadAction"]); }); test("Update the list title from the side panel", async function () { const { model, env } = await createSpreadsheetFromListView(); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); const target = await contains(".os-input"); await target.click(); await target.edit("new name"); expect(model.getters.getListName(listId)).toBe("new name"); }); test("list with a contextual domain", async () => { // TODO: the date is coded at 12PM so the test won't fail if the timezone is not UTC. It will still fail on some // timezones (GMT +13). The good way to do the test would be to patch the time zone and the date correctly. // But PyDate uses new Date() instead of luxon, which cannot be correctly patched. mockDate("2016-05-14 12:00:00"); const serverData = getBasicServerData(); serverData.models.partner.records = [ { id: 1, probability: 0.5, date: "2016-05-14", }, ]; serverData.views["partner,false,search"] = /* xml */ ` `; serverData.views["partner,false,list"] = /* xml */ ` `; const { model } = await createSpreadsheetFromListView({ serverData, additionalContext: { search_default_filter: 1 }, mockRPC: function (route, args) { if (args.method === "web_search_read") { expect(args.kwargs.domain).toEqual([["date", "=", "2016-05-14"]], { message: "data should be fetched with the evaluated the domain", }); expect.step("web_search_read"); } }, }); const listId = "1"; expect(model.getters.getListDefinition(listId).domain).toEqual( '[("date", "=", context_today())]' ); expect(model.exportData().lists[listId].domain).toBe('[("date", "=", context_today())]', { message: "domain is exported with the dynamic value", }); expect.verifySteps([ "web_search_read", // list view is loaded "web_search_read", // the data is loaded in the spreadsheet ]); }); test("Update the list domain from the side panel", async function () { onRpc("/web/domain/validate", () => true); const { model, env } = await createSpreadsheetFromListView(); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); const fixture = getFixture(); await contains(".o_edit_domain").click(); await dsHelpers.addNewRule(); await contains(".modal-footer .btn-primary").click(); expect(model.getters.getListDefinition(listId).domain).toEqual([["id", "=", 1]]); expect(dsHelpers.getConditionText(fixture)).toBe("Id = 1"); }); test("Update the list sorting from the side panel", async function () { const { model, env } = await createSpreadsheetFromListView(); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); await contains(".add-dimension").click(); for (let i = 0; i < 5; i++) { await contains(".o-popover input").press("ArrowDown"); } await contains(".o-autocomplete-value-focus").click(); expect(model.getters.getListDefinition(listId).orderBy).toEqual([{ name: "date", asc: true }]); await contains(".add-dimension").click(); for (let i = 0; i < 7; i++) { await contains(".o-popover input").press("ArrowDown"); } await contains(".o-autocomplete-value-focus").click(); const fixture = getFixture(); fixture.querySelector(".o-select-order").value = false; fixture.querySelector(".o-select-order").dispatchEvent(new Event("change")); expect(model.getters.getListDefinition(listId).orderBy).toEqual([ { name: "date", asc: false }, { name: "foo", asc: true }, ]); model.dispatch("REQUEST_UNDO"); expect(model.getters.getListDefinition(listId).orderBy).toEqual([ { name: "date", asc: true }, { name: "foo", asc: true }, ]); await contains(".o-delete-rule").click(); expect(model.getters.getListDefinition(listId).orderBy).toEqual([{ name: "foo", asc: true }]); }); test("List sorting selector should display only sortable fields", async function () { Partner._fields.foo = fields.Integer({ sortable: false }); Partner._fields.bar = fields.Boolean({ sortable: false }); const { model, env } = await createSpreadsheetFromListView({}); const [listId] = model.getters.getListIds(); const fixture = getFixture(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); await contains(".add-dimension").click(); const options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")]; const availableFields = options.map((el) => el.innerText); expect(availableFields).toEqual([ "Active", "Creation Date", "Currency", "Date", "Display name", "field_with_array_agg", "Id", "Json Field", "Last Modified on", "Money!", "name", "Probability", "Product", "Properties", "Tags", "Users", ]); }); test("List sorting selector should not display already used fields", async function () { Partner._fields.foo = fields.Integer({ sortable: false }); Partner._fields.bar = fields.Boolean({ sortable: false }); const { model, env } = await createSpreadsheetFromListView({}); const [listId] = model.getters.getListIds(); const fixture = getFixture(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); await contains(".add-dimension").click(); await contains(".o-popover input").press("ArrowDown"); await contains(".o-popover input").press("ArrowDown"); await contains(".o-autocomplete-value-focus").click(); await contains(".add-dimension").click(); let options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")]; let availableFields = options.map((el) => el.innerText); expect(availableFields).toEqual([ "Active", /* "Creation Date", this field should not be available anymore as it is used */ "Currency", "Date", "Display name", "field_with_array_agg", "Id", "Json Field", "Last Modified on", "Money!", "name", "Probability", "Product", "Properties", "Tags", "Users", ]); await contains(".o-popover input").press("ArrowDown"); await contains(".o-popover input").press("ArrowDown"); await contains(".o-autocomplete-value-focus").click(); await contains(".add-dimension").click(); options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")]; availableFields = options.map((el) => el.innerText); expect(availableFields).toEqual([ "Active", /* "Creation Date", this field should not be available anymore as it is used */ /* "Currency", this field should not be available anymore as it is used */ "Date", "Display name", "field_with_array_agg", "Id", "Json Field", "Last Modified on", "Money!", "name", "Probability", "Product", "Properties", "Tags", "Users", ]); }); test("List sorting selector should display used fields again when corresponding rule is deleted", async function () { Partner._fields.foo = fields.Integer({ sortable: false }); Partner._fields.bar = fields.Boolean({ sortable: false }); const { model, env } = await createSpreadsheetFromListView({}); const [listId] = model.getters.getListIds(); const fixture = getFixture(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); await contains(".add-dimension").click(); await contains(".o-popover input").press("ArrowDown"); await contains(".o-popover input").press("ArrowDown"); await contains(".o-autocomplete-value-focus").click(); await contains(".add-dimension").click(); let options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")]; let availableFields = options.map((el) => el.innerText); expect(availableFields).toEqual([ "Active", /* "Creation Date", this field should not be available anymore as it is used */ "Currency", "Date", "Display name", "field_with_array_agg", "Id", "Json Field", "Last Modified on", "Money!", "name", "Probability", "Product", "Properties", "Tags", "Users", ]); await contains(".o-delete-rule").click(); await contains(".add-dimension").click(); options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")]; availableFields = options.map((el) => el.innerText); expect(availableFields).toEqual([ "Active", "Creation Date", "Currency", "Date", "Display name", "field_with_array_agg", "Id", "Json Field", "Last Modified on", "Money!", "name", "Probability", "Product", "Properties", "Tags", "Users", ]); }); test("Inserting a list preserves the ascending sorting from the list", async function () { const serverData = getBasicServerData(); Partner._fields.foo.sortable = true; const { model } = await createSpreadsheetFromListView({ serverData, orderBy: [{ name: "foo", asc: true }], linesNumber: 4, }); expect(getEvaluatedCell(model, "A2").value <= getEvaluatedCell(model, "A3").value).not.toBe( undefined ); expect(getEvaluatedCell(model, "A3").value <= getEvaluatedCell(model, "A4").value).not.toBe( undefined ); expect(getEvaluatedCell(model, "A4").value <= getEvaluatedCell(model, "A5").value).not.toBe( undefined ); }); test("Inserting a list preserves the descending sorting from the list", async function () { const serverData = getBasicServerData(); Partner._fields.foo.sortable = true; const { model } = await createSpreadsheetFromListView({ serverData, orderBy: [{ name: "foo", asc: false }], linesNumber: 4, }); expect(getEvaluatedCell(model, "A2").value >= getEvaluatedCell(model, "A3").value).not.toBe( undefined ); expect(getEvaluatedCell(model, "A3").value >= getEvaluatedCell(model, "A4").value).not.toBe( undefined ); expect(getEvaluatedCell(model, "A4").value >= getEvaluatedCell(model, "A5").value).not.toBe( undefined ); }); test("Sorting from the list is displayed in the properties panel", async function () { const serverData = getBasicServerData(); Partner._fields.foo.sortable = true; Partner._fields.bar.sortable = true; const { model, env } = await createSpreadsheetFromListView({ serverData, orderBy: [ { name: "foo", asc: true }, { name: "bar", asc: false }, ], linesNumber: 4, }); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); expect(".o_sorting_rule_column:eq(0)").toHaveText("Bar"); expect(".o_sorting_rule_column:eq(1)").toHaveText("Foo"); /** Bar should be descending */ expect(".o-select-order:eq(0)").toHaveValue("false"); /** Foo should be ascending */ expect(".o-select-order:eq(1)").toHaveValue("true"); }); test("Opening the sidepanel of a list while the panel of another list is open updates the side panel", async function () { const { model, env } = await createSpreadsheetFromListView({}); insertListInSpreadsheet(model, { model: "product", columns: ["name", "active"], }); const listIds = model.getters.getListIds(); const fixture = getFixture(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId: listIds[0] }); await animationFrame(); let modelName = fixture.querySelector(".o-section .o_model_name"); expect(modelName).toHaveText("Partner (partner)"); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId: listIds[1] }); await animationFrame(); modelName = fixture.querySelector(".o-section .o_model_name"); expect(modelName).toHaveText("Product (product)"); }); test("Duplicate a list from the side panel", async function () { const serverData = getBasicServerData(); Partner._fields.foo.sortable = true; const { model, env } = await createSpreadsheetFromListView({ serverData, orderBy: [{ name: "foo", asc: true }], }); const [listId] = model.getters.getListIds(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId }); await animationFrame(); expect(model.getters.getListIds().length).toBe(1); expect(".o-sidePanelTitle").toHaveText("List #1"); await contains(".os-cog-wheel-menu-icon").click(); await contains(".o-popover .fa-clone").click(); expect(model.getters.getListIds().length).toBe(2); expect(".o-sidePanelTitle").toHaveText("List #2"); }); test("List export from an action with an xml ID", async function () { const actionXmlId = "spreadsheet.partner_action"; const { model } = await createSpreadsheetFromListView({ actionXmlId }); expect(model.getters.getListDefinition("1").actionXmlId).toBe("spreadsheet.partner_action"); }); test("List cells are highlighted when their side panel is open", async function () { const { model, env } = await createSpreadsheetFromListView(); const sheetId = model.getters.getActiveSheetId(); env.openSidePanel("LIST_PROPERTIES_PANEL", { listId: "1" }); await animationFrame(); const range = model.getters.getRangeFromZone( sheetId, getZoneOfInsertedDataSource(model, "list", "1") ); expect(getHighlightsFromStore(env)).toEqual([{ range, noFill: true }]); await contains(".o-sidePanelClose").click(); expect(getHighlightsFromStore(env)).toEqual([]); }); test("List cells are highlighted when hovering the list menu item", async function () { const { model, env } = await createSpreadsheetFromListView(); await contains(".o-sidePanelClose").click(); const sheetId = model.getters.getActiveSheetId(); await contains(".o-topbar-top div[data-id='data']").click(); await hover("div[data-name='item_list_1']"); const range = model.getters.getRangeFromZone( sheetId, getZoneOfInsertedDataSource(model, "list", "1") ); expect(getHighlightsFromStore(env)).toEqual([{ range, noFill: true }]); await leave("div[data-name='item_list_1']"); expect(getHighlightsFromStore(env)).toEqual([]); }); test("Inserting a grouped list ignore groups", async function () { const serverData = getBasicServerData(); Partner._fields.foo.sortable = true; onRpc("partner", "web_read_group", ({ kwargs }) => { if (kwargs.groupby) { // The mock server cannot handle orderby count kwargs.order = ""; } }); const { model } = await createSpreadsheetFromListView({ actions: async (fixture) => { // display the property which is an optional column await contains(".o_searchview_dropdown_toggler").click(); await contains(".o_add_custom_group_menu").select("bar"); await contains(".o_searchview_facet_label").click(); }, serverData, linesNumber: 4, }); expect(getEvaluatedCell(model, "A2").value <= getEvaluatedCell(model, "A3").value).not.toBe( undefined ); }); test("Middle-click on 'See record' opens the record in a new tab", async function () { const { model, env } = await createSpreadsheetFromListView({ serverData: { models: getBasicData(), views: { "partner,false,list": ` `, "partner,false,search": "", }, }, }); mockService("action", { doAction(_, options) { expect.step("doAction"); expect(options).toEqual({ newWindow: true, viewType: "form", }); return Promise.resolve(true); }, }); selectCell(model, "A2"); const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record"); await root.execute(env, true); expect.verifySteps(["doAction"]); }); test("readonly list side panel", async function () { const { model, env } = await createSpreadsheetFromListView(); await doMenuAction(topbarMenuRegistry, ["data", "item_list_1"], env); await animationFrame(); expect(".o-sidePanelBody > div.pe-none.opacity-50").toHaveCount(0); model.updateMode("readonly"); await animationFrame(); expect(".o-sidePanelBody > div.pe-none.opacity-50").toHaveCount(1); expect(".o-sidePanelBody > div.pe-none.opacity-50").toHaveAttribute("inert"); }); test("display list related filters panel", async function () { const { model } = await createSpreadsheetFromListView(); const listId = model.getters.getListIds()[0]; await addGlobalFilter( model, { id: "42", type: "relation", label: "Filter" }, { list: { [listId]: { chain: "product_id", type: "many2one", }, }, } ); await addGlobalFilter(model, { id: "43", type: "relation", label: "Filter 2" }); expect(".o_side_panel_collapsible_title:contains(Matching 1 / 2 filters)").toHaveCount(1); });