From 96e158a8b8bf3d9adf8bc634cf690c7f3a0cf829 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Tue, 6 Dec 2022 07:26:06 +0000 Subject: [PATCH] [REF] web: simplify customizations of static menu actions There are several customizations of the list and form view that consist in modifying the behavior of a static menu action (archive, unarchive, export, delete, duplicate). We have seen that this one is very complex. So we decided to simplify it. Solution: Add the getStaticActionMenuItems API point. This allows us to easily modify the behaviour of static actions. We also took advantage of this commit to simplify the ActionMenu api. We have removed the other actions because it was not clear enough. They are directly added in the action category. closes odoo/odoo#107086 Taskid: 3089039 Related: odoo/enterprise#34598 Signed-off-by: Aaron Bohy (aab) --- addons/fleet/static/src/js/fleet_form.js | 29 ++--- addons/hr/static/src/views/form_view.js | 13 +-- addons/hr/static/src/views/list_view.js | 13 +-- .../form/project_sharing_form_controller.js | 2 +- .../project_task_form_controller.js | 24 ++-- .../project_task_list_controller.js | 33 ++---- .../list/inventory_report_list_controller.js | 21 ++-- .../src/search/action_menus/action_menus.js | 43 +++----- .../static/src/views/form/form_controller.js | 92 +++++++++------- .../static/src/views/form/form_controller.xml | 2 +- .../static/src/views/list/list_controller.js | 60 ++++++---- .../static/src/views/list/list_controller.xml | 2 +- addons/web/static/tests/helpers/mock_env.js | 1 - .../tests/views/form/form_view_tests.js | 103 ++++++++++++++++++ .../web/static/tests/views/list_view_tests.js | 102 +++++++++++++++++ .../static/src/components/views/page_list.js | 33 +++--- 16 files changed, 374 insertions(+), 199 deletions(-) diff --git a/addons/fleet/static/src/js/fleet_form.js b/addons/fleet/static/src/js/fleet_form.js index 6cb35497a0b..50808f6b2e8 100644 --- a/addons/fleet/static/src/js/fleet_form.js +++ b/addons/fleet/static/src/js/fleet_form.js @@ -9,25 +9,18 @@ export class FleetFormController extends FormController { /** * @override **/ - getActionMenuItems() { - if (this.model.root.isInEdition) { - return {}; - } - - const menuItems = super.getActionMenuItems(); - const archiveAction = menuItems.other.find((item) => item.key === "archive"); - if (archiveAction) { - archiveAction.callback = () => { - const dialogProps = { - body: this.env._t( - "Every service and contract of this vehicle will be considered as archived. Are you sure that you want to archive this record?" - ), - confirm: () => this.model.root.archive(), - cancel: () => {}, - }; - this.dialogService.add(ConfirmationDialog, dialogProps); + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.archive.callback = () => { + const dialogProps = { + body: this.env._t( + "Every service and contract of this vehicle will be considered as archived. Are you sure that you want to archive this record?" + ), + confirm: () => this.model.root.archive(), + cancel: () => {}, }; - } + this.dialogService.add(ConfirmationDialog, dialogProps); + }; return menuItems; } } diff --git a/addons/hr/static/src/views/form_view.js b/addons/hr/static/src/views/form_view.js index b9aa209af4b..cdaed123937 100644 --- a/addons/hr/static/src/views/form_view.js +++ b/addons/hr/static/src/views/form_view.js @@ -15,16 +15,9 @@ export class EmployeeFormController extends FormController { this.archiveEmployee = useArchiveEmployee(); } - getActionMenuItems() { - const menuItems = super.getActionMenuItems(); - if (!this.archiveEnabled || !this.model.root.isActive) { - return menuItems; - } - - const archiveAction = menuItems.other.find((item) => item.key === "archive"); - if (archiveAction) { - archiveAction.callback = this.archiveEmployee.bind(this, this.model.root.resId); - } + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.archive.callback = this.archiveEmployee.bind(this, this.model.root.resId); return menuItems; } } diff --git a/addons/hr/static/src/views/list_view.js b/addons/hr/static/src/views/list_view.js index 6f1a322c5a6..803c44259a3 100644 --- a/addons/hr/static/src/views/list_view.js +++ b/addons/hr/static/src/views/list_view.js @@ -13,18 +13,13 @@ export class EmployeeListController extends ListController { this.archiveEmployee = useArchiveEmployee(); } - getActionMenuItems() { - const menuItems = super.getActionMenuItems(); + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); const selectedRecords = this.model.root.selection; // Only override the Archive action when only 1 record is selected. - if (!this.archiveEnabled || selectedRecords.length > 1 || !selectedRecords[0].data.active) { - return menuItems; - } - - const archiveAction = menuItems.other.find((item) => item.key === "archive"); - if (archiveAction) { - archiveAction.callback = this.archiveEmployee.bind(this, selectedRecords[0].resId); + if (selectedRecords.length === 1 && selectedRecords[0].data.active) { + menuItems.archive.callback = this.archiveEmployee.bind(this, selectedRecords[0].resId); } return menuItems; } diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js index 652ab0b5b32..11ca80770d4 100644 --- a/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js @@ -20,7 +20,7 @@ export class ProjectSharingFormController extends FormController { this.mailTemplate = mailTemplates.Mail; } - getActionMenuItems() { + get actionMenuItems() { return {}; } } diff --git a/addons/project/static/src/views/project_task_form/project_task_form_controller.js b/addons/project/static/src/views/project_task_form/project_task_form_controller.js index 9f929de9345..979f7c1a514 100644 --- a/addons/project/static/src/views/project_task_form/project_task_form_controller.js +++ b/addons/project/static/src/views/project_task_form/project_task_form_controller.js @@ -9,23 +9,15 @@ export class ProjectTaskFormController extends FormController { this.taskRecurrence = useService('project_task_recurrence'); } - getActionMenuItems() { - if (!(this.archiveEnabled && this.model.root.isActive) || !this.model.root.data.recurrence_id) { - return super.getActionMenuItems(); + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + if (this.model.root.data.recurrence_id) { + menuItems.archive.callback = () => + this.taskRecurrence.stopRecurrence([this.model.root], () => + this.model.root.archive() + ); } - this.archiveEnabled = false; - const actionMenuItems = super.getActionMenuItems(); - this.archiveEnabled = true; - if (actionMenuItems) { - actionMenuItems.other.unshift({ - description: this.env._t('Archive'), - callback: () => this.taskRecurrence.stopRecurrence( - [this.model.root], - () => this.model.root.archive(), - ), - }); - } - return actionMenuItems; + return menuItems; } deleteRecord() { diff --git a/addons/project/static/src/views/project_task_list/project_task_list_controller.js b/addons/project/static/src/views/project_task_list/project_task_list_controller.js index 069a9dfc018..bc1d9480429 100644 --- a/addons/project/static/src/views/project_task_list/project_task_list_controller.js +++ b/addons/project/static/src/views/project_task_list/project_task_list_controller.js @@ -9,32 +9,17 @@ export class ProjectTaskListController extends ListController { this.taskRecurrence = useService('project_task_recurrence'); } - getActionMenuItems() { - if (!this.archiveEnabled || this.model.root.isM2MGrouped) { - return super.getActionMenuItems(); - } + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); const hasAnyRecurrences = this._anySelectedTasksWithRecurrence(); - this.archiveEnabled = !hasAnyRecurrences; - const actionMenuItems = super.getActionMenuItems(); - this.archiveEnabled = true; - if (actionMenuItems && hasAnyRecurrences) { - actionMenuItems.other.splice( - this.isExportEnable ? 1 : 0, - 0, - { - description: this.env._t('Archive'), - callback: () => this.taskRecurrence.stopRecurrence( - this.model.root.selection, - () => this.toggleArchiveState(true), - ), - }, - { - description: this.env._t('Unarchive'), - callback: () => this.toggleArchiveState(false), - }, - ); + + if (hasAnyRecurrences) { + menuItems.archive.callback = () => + this.taskRecurrence.stopRecurrence(this.model.root.selection, () => + this.toggleArchiveState(true) + ); } - return actionMenuItems; + return menuItems; } onDeleteSelectedRecords() { diff --git a/addons/stock/static/src/views/list/inventory_report_list_controller.js b/addons/stock/static/src/views/list/inventory_report_list_controller.js index a5065acab84..f4f1108f94a 100644 --- a/addons/stock/static/src/views/list/inventory_report_list_controller.js +++ b/addons/stock/static/src/views/list/inventory_report_list_controller.js @@ -35,17 +35,18 @@ export class InventoryReportListController extends ListController { }); } - getActionMenuItems() { - const actionMenus = super.getActionMenuItems(); - if (this.props.resModel === "stock.quant" && (!this.props.context.inventory_mode || this.props.context.inventory_report_mode)) { + get actionMenuItems() { + const actionMenus = super.actionMenuItems; + if ( + this.props.resModel === "stock.quant" && + (!this.props.context.inventory_mode || this.props.context.inventory_report_mode) + ) { // hack so we don't show some of the default actions when it's inappropriate to - const {print, action, other} = actionMenus; - return Object.assign( - {}, - print.filter(a => a.name !== 'Count Sheet'), - action.filter(a => a.name !== 'Set'), - { other: other }, - ); + const { print, action } = actionMenus; + return { + action: action.filter((a) => a.name !== "Set"), + print: print.filter((a) => a.name !== "Count Sheet"), + }; } return actionMenus; } diff --git a/addons/web/static/src/search/action_menus/action_menus.js b/addons/web/static/src/search/action_menus/action_menus.js index 7b5defe0e7f..00027d843df 100644 --- a/addons/web/static/src/search/action_menus/action_menus.js +++ b/addons/web/static/src/search/action_menus/action_menus.js @@ -3,13 +3,11 @@ import { browser } from "@web/core/browser/browser"; import { makeContext } from "@web/core/context"; import { session } from "@web/session"; -import { registry } from "@web/core/registry"; import { Dropdown } from "@web/core/dropdown/dropdown"; import { DropdownItem } from "@web/core/dropdown/dropdown_item"; import { useService } from "@web/core/utils/hooks"; import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl"; -let registryActionId = 0; /** * Action menus (or Action/Print bar, previously called 'Sidebar') * @@ -25,10 +23,10 @@ export class ActionMenus extends Component { this.orm = useService("orm"); this.actionService = useService("action"); onWillStart(async () => { - this.actionItems = await this.setActionItems(this.props); + this.actionItems = await this.getActionItems(this.props); }); onWillUpdateProps(async (nextProps) => { - this.actionItems = await this.setActionItems(nextProps); + this.actionItems = await this.getActionItems(nextProps); }); } @@ -45,32 +43,18 @@ export class ActionMenus extends Component { // Private //--------------------------------------------------------------------- - async setActionItems(props) { - // Callback based actions - const callbackActions = (props.items.other || []).map((action) => - Object.assign({ key: `action-${action.description}` }, action) - ); - // Action based actions - const actionActions = props.items.action || []; - const formattedActions = actionActions.map((action) => ({ - action, - description: action.name, - key: action.id, - })); - // ActionMenus action registry components - const registryActions = []; - for (const { Component, getProps } of registry.category("action_menus").getAll()) { - const itemProps = await getProps(props, this.env); - if (itemProps) { - registryActions.push({ - Component, - key: `registry-action-${registryActionId++}`, - props: itemProps, - }); + async getActionItems(props) { + return (props.items.action || []).map((action) => { + if (action.callback) { + return Object.assign({ key: `action-${action.description}` }, action); + } else { + return { + action, + description: action.name, + key: action.id, + }; } - } - - return [...callbackActions, ...formattedActions, ...registryActions]; + }); } //--------------------------------------------------------------------- @@ -141,7 +125,6 @@ ActionMenus.props = { shape: { action: { type: Array, optional: true }, print: { type: Array, optional: true }, - other: { type: Array, optional: true }, }, }, onActionExecuted: { type: Function, optional: true }, diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js index 2085696fc34..1d57981c92f 100644 --- a/addons/web/static/src/views/form/form_controller.js +++ b/addons/web/static/src/views/form/form_controller.js @@ -1,11 +1,13 @@ /** @odoo-module **/ +import { hasTouch } from "@web/core/browser/feature_detection"; import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import { makeContext } from "@web/core/context"; import { useDebugCategory } from "@web/core/debug/debug_context"; import { registry } from "@web/core/registry"; import { SIZES } from "@web/core/ui/ui_service"; import { useBus, useService } from "@web/core/utils/hooks"; +import { omit } from "@web/core/utils/objects"; import { createElement } from "@web/core/utils/xml"; import { ActionMenus } from "@web/search/action_menus/action_menus"; import { Layout } from "@web/search/layout"; @@ -15,10 +17,9 @@ import { standardViewProps } from "@web/views/standard_view_props"; import { isX2Many } from "@web/views/utils"; import { useViewButtons } from "@web/views/view_button/view_button_hook"; import { useSetupView } from "@web/views/view_hook"; -import { hasTouch } from "@web/core/browser/feature_detection"; import { FormStatusIndicator } from "./form_status_indicator/form_status_indicator"; -import { Component, onWillStart, useEffect, useRef, onRendered, useState, toRaw } from "@odoo/owl"; +import { Component, onRendered, onWillStart, toRaw, useEffect, useRef, useState } from "@odoo/owl"; const viewRegistry = registry.category("views"); @@ -75,7 +76,11 @@ export async function loadSubViews( refinedContext.base_model_name = resModel; const comodel = field.relation; - const { fields: comodelFields, relatedModels, views } = await viewService.loadViews({ + const { + fields: comodelFields, + relatedModels, + views, + } = await viewService.loadViews({ resModel: comodel, views: [[false, viewType]], context: makeContext([fieldContext, userService.context, refinedContext]), @@ -304,48 +309,55 @@ export class FormController extends Component { this.router.pushState({ id: this.model.root.resId || undefined }); } - getActionMenuItems() { - const otherActionItems = []; - if (this.archiveEnabled) { - if (this.model.root.isActive) { - otherActionItems.push({ - key: "archive", - description: this.env._t("Archive"), - callback: () => { - const dialogProps = { - body: this.env._t( - "Are you sure that you want to archive all this record?" - ), - confirm: () => this.model.root.archive(), - cancel: () => {}, - }; - this.dialogService.add(ConfirmationDialog, dialogProps); - }, - }); - } else { - otherActionItems.push({ - key: "unarchive", - description: this.env._t("Unarchive"), - callback: () => this.model.root.unarchive(), - }); - } - } - if (this.archInfo.activeActions.create && this.archInfo.activeActions.duplicate) { - otherActionItems.push({ - key: "duplicate", + getStaticActionMenuItems() { + const { activeActions } = this.archInfo; + return { + archive: { + isAvailable: () => this.archiveEnabled && this.model.root.isActive, + sequence: 10, + description: this.env._t("Archive"), + callback: () => { + const dialogProps = { + body: this.env._t("Are you sure that you want to archive all this record?"), + confirm: () => this.model.root.archive(), + cancel: () => {}, + }; + this.dialogService.add(ConfirmationDialog, dialogProps); + }, + }, + unarchive: { + isAvailable: () => this.archiveEnabled && !this.model.root.isActive, + sequence: 20, + description: this.env._t("Unarchive"), + callback: () => this.model.root.unarchive(), + }, + duplicate: { + isAvailable: () => activeActions.create && activeActions.duplicate, + sequence: 30, description: this.env._t("Duplicate"), callback: () => this.duplicateRecord(), - }); - } - if (this.archInfo.activeActions.delete && !this.model.root.isVirtual) { - otherActionItems.push({ - key: "delete", + }, + delete: { + isAvailable: () => activeActions.delete && !this.model.root.isVirtual, + sequence: 40, description: this.env._t("Delete"), callback: () => this.deleteRecord(), skipSave: true, - }); - } - return Object.assign({}, this.props.info.actionMenus, { other: otherActionItems }); + }, + }; + } + + get actionMenuItems() { + const { actionMenus } = this.props.info; + const staticActionItems = Object.entries(this.getStaticActionMenuItems()) + .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable()) + .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0)) + .map(([key, item]) => Object.assign({ key }, omit(item, "isAvailable", "sequence"))); + + return { + action: [...staticActionItems, ...(actionMenus.action || [])], + print: actionMenus.print, + }; } async shouldExecuteAction(item) { diff --git a/addons/web/static/src/views/form/form_controller.xml b/addons/web/static/src/views/form/form_controller.xml index bc98334ac4a..a8f00f4047d 100644 --- a/addons/web/static/src/views/form/form_controller.xml +++ b/addons/web/static/src/views/form/form_controller.xml @@ -18,7 +18,7 @@ this.isExportEnable, + sequence: 10, description: this.env._t("Export"), callback: () => this.onExportData(), - }); - } - if (this.archiveEnabled && !isM2MGrouped) { - otherActionItems.push({ - key: "archive", + }, + archive: { + isAvailable: () => this.archiveEnabled && !isM2MGrouped, + sequence: 20, description: this.env._t("Archive"), callback: () => { const dialogProps = { @@ -279,21 +279,33 @@ export class ListController extends Component { }; this.dialogService.add(ConfirmationDialog, dialogProps); }, - }); - otherActionItems.push({ - key: "unarchive", + }, + unarchive: { + isAvailable: () => this.archiveEnabled && !isM2MGrouped, + sequence: 30, description: this.env._t("Unarchive"), callback: () => this.toggleArchiveState(false), - }); - } - if (this.activeActions.delete && !isM2MGrouped) { - otherActionItems.push({ - key: "delete", + }, + delete: { + isAvailable: () => this.activeActions.delete && !isM2MGrouped, + sequence: 40, description: this.env._t("Delete"), callback: () => this.onDeleteSelectedRecords(), - }); - } - return Object.assign({}, this.props.info.actionMenus, { other: otherActionItems }); + }, + }; + } + + get actionMenuItems() { + const { actionMenus } = this.props.info; + const staticActionItems = Object.entries(this.getStaticActionMenuItems()) + .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable()) + .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0)) + .map(([key, item]) => Object.assign({ key }, omit(item, "isAvailable", "sequence"))); + + return { + action: [...staticActionItems, ...(actionMenus.action || [])], + print: actionMenus.print, + }; } async onSelectDomain() { diff --git a/addons/web/static/src/views/list/list_controller.xml b/addons/web/static/src/views/list/list_controller.xml index 8b5d87108c5..a63c874bd99 100644 --- a/addons/web/static/src/views/list/list_controller.xml +++ b/addons/web/static/src/views/list/list_controller.xml @@ -20,7 +20,7 @@ getActiveIds="() => model.root.selection.map((r) => r.resId)" context="props.context" domain="props.domain" - items="getActionMenuItems()" + items="actionMenuItems" isDomainSelected="model.root.isDomainSelected" resModel="model.root.resModel" onActionExecuted="() => model.load()"/> diff --git a/addons/web/static/tests/helpers/mock_env.js b/addons/web/static/tests/helpers/mock_env.js index d30ace132f5..7dde26e59b6 100644 --- a/addons/web/static/tests/helpers/mock_env.js +++ b/addons/web/static/tests/helpers/mock_env.js @@ -73,7 +73,6 @@ function prepareRegistriesWithCleanup() { clearRegistryWithCleanup(registry.category("user_menuitems")); clearRegistryWithCleanup(registry.category("kanban_examples")); clearRegistryWithCleanup(registry.category("__processed_archs__")); - clearRegistryWithCleanup(registry.category("action_menus")); // fun fact: at least one registry is missing... this shows that we need a // better design for the way we clear these registries... } diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 6f3dc902727..675e26c9c77 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -3845,6 +3845,109 @@ QUnit.module("Views", (hooks) => { ]); }); + QUnit.test("apply custom standard action menu (archive)", async function (assert) { + // add active field on partner model to have archive option + serverData.models.partner.fields.active = { string: "Active", type: "char", default: true }; + + const formView = registry.category("views").get("form"); + class CustomFormController extends formView.Controller { + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.archive.callback = () => { + assert.step("customArchive"); + }; + return menuItems; + } + } + registry.category("views").add("custom_form", { + ...formView, + Controller: CustomFormController, + }); + + await makeView({ + type: "form", + resModel: "partner", + serverData, + resId: 1, + actionMenus: {}, + arch: ` +
+ + + `, + }); + + assert.containsOnce(target, ".o_cp_action_menus"); + + await toggleActionMenu(target); + assert.containsOnce(target, ".o_cp_action_menus span:contains(Archive)"); + + await toggleMenuItem(target, "Archive"); + assert.verifySteps(["customArchive"]); + }); + + QUnit.test("add custom static action menu", async function (assert) { + const formView = registry.category("views").get("form"); + class CustomFormController extends formView.Controller { + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.customAvailable = { + isAvailable: () => true, + description: "Custom Available", + sequence: 35, + callback: () => { + assert.step("Custom Available"); + }, + }; + menuItems.customNotAvailable = { + isAvailable: () => false, + description: "Custom Not Available", + callback: () => { + assert.step("Custom Not Available"); + }, + }; + menuItems.customDefaultAvailable = { + description: "Custom Default Available", + callback: () => { + assert.step("Custom Default Available"); + }, + }; + return menuItems; + } + } + registry.category("views").add("custom_form", { + ...formView, + Controller: CustomFormController, + }); + + await makeView({ + type: "form", + resModel: "partner", + serverData, + resId: 1, + actionMenus: {}, + arch: ` +
+ + `, + }); + + assert.containsOnce(target, ".o_cp_action_menus"); + + await toggleActionMenu(target); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_cp_action_menus .dropdown-item")), + ["Custom Default Available", "Duplicate", "Custom Available", "Delete"] + ); + + await toggleMenuItem(target, "Custom Available"); + assert.verifySteps(["Custom Available"]); + + await toggleActionMenu(target); + await toggleMenuItem(target, "Custom Default Available"); + assert.verifySteps(["Custom Default Available"]); + }); + QUnit.test("archive a record with intermediary action", async function (assert) { // add active field on partner model to have archive option serverData.models.partner.fields.active = { string: "Active", type: "char", default: true }; diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 144ebc51b5b..09892dac735 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -5334,6 +5334,108 @@ QUnit.module("Views", (hooks) => { ); }); + QUnit.test("apply custom static action menu (archive)", async function (assert) { + // add active field on foo model and make all records active + serverData.models.foo.fields.active = { string: "Active", type: "boolean", default: true }; + + const listView = registry.category("views").get("list"); + class CustomListController extends listView.Controller { + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.archive.callback = () => { + assert.step("customArchive"); + }; + return menuItems; + } + } + registry.category("views").add("custom_list", { + ...listView, + Controller: CustomListController, + }); + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: ` + + + `, + actionMenus: {}, + }); + assert.containsNone(target, "div.o_control_panel .o_cp_action_menus"); + + await click(target, "thead .o_list_record_selector input"); + assert.containsOnce(target, "div.o_control_panel .o_cp_action_menus"); + + await toggleActionMenu(target); + await toggleMenuItem(target, "Archive"); + assert.verifySteps(["customArchive"]); + }); + + QUnit.test("add custom static action menu", async function (assert) { + const listView = registry.category("views").get("list"); + class CustomListController extends listView.Controller { + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.customAvailable = { + isAvailable: () => true, + description: "Custom Available", + sequence: 35, + callback: () => { + assert.step("Custom Available"); + }, + }; + menuItems.customNotAvailable = { + isAvailable: () => false, + description: "Custom Not Available", + callback: () => { + assert.step("Custom Not Available"); + }, + }; + menuItems.customDefaultAvailable = { + description: "Custom Default Available", + callback: () => { + assert.step("Custom Default Available"); + }, + }; + return menuItems; + } + } + registry.category("views").add("custom_list", { + ...listView, + Controller: CustomListController, + }); + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: ` + + + `, + actionMenus: {}, + }); + assert.containsNone(target, "div.o_control_panel .o_cp_action_menus"); + + await click(target, "thead .o_list_record_selector input"); + assert.containsOnce(target, "div.o_control_panel .o_cp_action_menus"); + + await toggleActionMenu(target); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_cp_action_menus .dropdown-item")), + ["Custom Default Available", "Export", "Custom Available", "Delete"] + ); + + await toggleMenuItem(target, "Custom Available"); + assert.verifySteps(["Custom Available"]); + + await toggleActionMenu(target); + await toggleMenuItem(target, "Custom Default Available"); + assert.verifySteps(["Custom Default Available"]); + }); + QUnit.test( "grouped, update the count of the group (and ancestors) when a record is deleted", async function (assert) { diff --git a/addons/website/static/src/components/views/page_list.js b/addons/website/static/src/components/views/page_list.js index 5a39a4f4503..b1724be600c 100644 --- a/addons/website/static/src/components/views/page_list.js +++ b/addons/website/static/src/components/views/page_list.js @@ -16,6 +16,9 @@ export class PageListController extends PageControllerMixin(listView.Controller) setup() { super.setup(); this.orm = useService('orm'); + if (this.props.resModel === "website.page") { + this.archiveEnabled = false; + } } /** @@ -30,28 +33,30 @@ export class PageListController extends PageControllerMixin(listView.Controller) * * @override */ - getActionMenuItems() { - const actionMenuItems = super.getActionMenuItems(); - // 'Archive' / 'Unarchive' options are disabled only on 'website.page' list view. - if (this.props.resModel === 'website.page') { - actionMenuItems.other = actionMenuItems.other - .filter(item => !['archive', 'unarchive'].includes(item.key)); - } - actionMenuItems.other.splice(-1, 0, { + getStaticActionMenuItems() { + const menuItems = super.getStaticActionMenuItems(); + menuItems.publish = { + sequence: 15, description: this.env._t("Publish"), callback: async () => { this.dialogService.add(ConfirmationDialog, { title: this.env._t("Publish Website Content"), - body: sprintf(this.env._t("%s record(s) selected, are you sure you want to publish them all?"), this.model.root.selection.length), + body: sprintf( + this.env._t( + "%s record(s) selected, are you sure you want to publish them all?" + ), + this.model.root.selection.length + ), confirm: () => this.togglePublished(true), }); - } - }, - { + }, + }; + menuItems.unpublish = { + sequence: 16, description: this.env._t("Unpublish"), callback: async () => this.togglePublished(false), - }); - return actionMenuItems; + }; + return menuItems; } onDeleteSelectedRecords() {