)]}'
{"version": 3, "sources": ["/web/static/src/views/graph/graph_arch_parser.js", "/web/static/src/views/graph/graph_controller.js", "/web/static/src/views/graph/graph_model.js", "/web/static/src/views/graph/graph_renderer.js", "/web/static/src/views/graph/graph_search_model.js", "/web/static/src/views/graph/graph_view.js", "/web/static/src/views/pivot/pivot_arch_parser.js", "/web/static/src/views/pivot/pivot_controller.js", "/web/static/src/views/pivot/pivot_header.js", "/web/static/src/views/pivot/pivot_model.js", "/web/static/src/views/pivot/pivot_renderer.js", "/web/static/src/views/pivot/pivot_search_model.js", "/web/static/src/views/pivot/pivot_view.js", "/mail/static/src/views/web/activity/activity_arch_parser.js", "/mail/static/src/views/web/activity/activity_cell.js", "/mail/static/src/views/web/activity/activity_compiler.js", "/mail/static/src/views/web/activity/activity_controller.js", "/mail/static/src/views/web/activity/activity_model.js", "/mail/static/src/views/web/activity/activity_record.js", "/mail/static/src/views/web/activity/activity_renderer.js", "/mail/static/src/views/web/activity/activity_view.js", "/crm/static/src/views/forecast_graph/forecast_graph_view.js", "/crm/static/src/views/forecast_pivot/forecast_pivot_view.js", "/stock/static/src/stock_forecasted/forecasted_graph.js", "/web_enterprise/static/src/views/pivot/pivot_renderer.js", "/web_map/static/src/map_view/map_arch_parser.js", "/web_map/static/src/map_view/map_controller.js", "/web_map/static/src/map_view/map_model.js", "/web_map/static/src/map_view/map_renderer.js", "/web_map/static/src/map_view/map_view.js", "/web_gantt/static/src/gantt_arch_parser.js", "/web_gantt/static/src/gantt_compiler.js", "/web_gantt/static/src/gantt_connector.js", "/web_gantt/static/src/gantt_controller.js", "/web_gantt/static/src/gantt_helpers.js", "/web_gantt/static/src/gantt_mock_server.js", "/web_gantt/static/src/gantt_model.js", "/web_gantt/static/src/gantt_popover.js", "/web_gantt/static/src/gantt_popover_in_dialog.js", "/web_gantt/static/src/gantt_renderer.js", "/web_gantt/static/src/gantt_renderer_controls.js", "/web_gantt/static/src/gantt_resize_badge.js", "/web_gantt/static/src/gantt_row_progress_bar.js", "/web_gantt/static/src/gantt_sample_server.js", "/web_gantt/static/src/gantt_view.js", "/web_cohort/static/src/cohort_arch_parser.js", "/web_cohort/static/src/cohort_controller.js", "/web_cohort/static/src/cohort_model.js", "/web_cohort/static/src/cohort_renderer.js", "/web_cohort/static/src/cohort_view.js", "/web_cohort/static/src/cohort_view_sample_server.js", "/hr/static/src/views/hr_graph_view.js", "/hr/static/src/views/hr_pivot_view.js", "/web_hierarchy/static/src/hierarchy_arch_parser.js", "/web_hierarchy/static/src/hierarchy_card.js", "/web_hierarchy/static/src/hierarchy_compiler.js", "/web_hierarchy/static/src/hierarchy_controller.js", "/web_hierarchy/static/src/hierarchy_model.js", "/web_hierarchy/static/src/hierarchy_node_draggable.js", "/web_hierarchy/static/src/hierarchy_renderer.js", "/web_hierarchy/static/src/hierarchy_view.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_card.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_model.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_renderer.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_view.js", "/helpdesk/static/src/views/helpdesk_ticket_graph/helpdesk_ticket_graph_model.js", "/helpdesk/static/src/views/helpdesk_ticket_graph/helpdesk_ticket_graph_view.js", "/helpdesk/static/src/views/helpdesk_ticket_pivot/helpdesk_ticket_pivot_model.js", "/helpdesk/static/src/views/helpdesk_ticket_pivot/helpdesk_ticket_pivot_view.js", "/hr_skills/static/src/views/skills_graph.js", "/web_grid/static/src/components/float_factor_grid_cell.js", "/web_grid/static/src/components/float_time_grid_cell.js", "/web_grid/static/src/components/float_toggle_grid_cell.js", "/web_grid/static/src/components/grid_cell.js", "/web_grid/static/src/components/grid_component/grid_component.js", "/web_grid/static/src/components/grid_row/grid_row.js", "/web_grid/static/src/components/many2one_grid_row/many2one_grid_row.js", "/web_grid/static/src/hooks/grid_cell_hook.js", "/web_grid/static/src/hooks/input_hook.js", "/web_grid/static/src/views/grid_arch_parser.js", "/web_grid/static/src/views/grid_controller.js", "/web_grid/static/src/views/grid_model.js", "/web_grid/static/src/views/grid_renderer.js", "/web_grid/static/src/views/grid_view.js", "/analytic_enterprise/static/src/analytic_line_grid/analytic_line_grid_model.js", "/analytic_enterprise/static/src/analytic_line_grid/analytic_line_grid_view.js", "/hr_gantt/static/src/hr_gantt_employee_avatar.js", "/hr_gantt/static/src/hr_gantt_renderer.js", "/hr_gantt/static/src/hr_gantt_view.js", "/hr_org_chart/static/src/views/hr_employee_hierarchy/hr_employee_hierarchy_card.js", "/hr_org_chart/static/src/views/hr_employee_hierarchy/hr_employee_hierarchy_renderer.js", "/hr_org_chart/static/src/views/hr_employee_hierarchy/hr_employee_hierarchy_view.js", "/spreadsheet_edition/static/src/assets/graph_view/graph_view.js", "/spreadsheet_edition/static/src/assets/pivot_view/pivot_view.js", "/stock_enterprise/static/src/map_view/map_model.js", "/stock_enterprise/static/src/map_view/map_renderer.js", "/stock_enterprise/static/src/map_view/map_view.js", "/web_studio/static/src/client_action/action_editor/action_editor.js", "/web_studio/static/src/client_action/app_creator/app_creator.js", "/web_studio/static/src/client_action/components/font_awesome_icon_selector/font_awesome_icon_selector.js", "/web_studio/static/src/client_action/components/sidebar_draggable_item/sidebar_draggable_item.js", "/web_studio/static/src/client_action/components/thumbnail_item/thumbnail_item.js", "/web_studio/static/src/client_action/editor/app_menu_editor/app_menu_editor.js", "/web_studio/static/src/client_action/editor/edition_flow.js", "/web_studio/static/src/client_action/editor/editor.js", "/web_studio/static/src/client_action/editor/editor_menu/editor_menu.js", "/web_studio/static/src/client_action/editor/new_model_item/new_model_item.js", "/web_studio/static/src/client_action/editor/new_view_dialogs/map_new_view_dialog.js", "/web_studio/static/src/client_action/editor/new_view_dialogs/new_view_dialog.js", "/web_studio/static/src/client_action/editor/studio_action_container.js", "/web_studio/static/src/client_action/icon_creator/icon_creator.js", "/web_studio/static/src/client_action/menu_creator/menu_creator.js", "/web_studio/static/src/client_action/model_configurator/model_configurator.js", "/web_studio/static/src/client_action/navbar/home_menu_customizer/home_menu_customizer.js", "/web_studio/static/src/client_action/navbar/navbar.js", "/web_studio/static/src/client_action/report_editor/error_display.js", "/web_studio/static/src/client_action/report_editor/report_editor.js", "/web_studio/static/src/client_action/report_editor/report_editor_iframe.js", "/web_studio/static/src/client_action/report_editor/report_editor_model.js", "/web_studio/static/src/client_action/report_editor/report_editor_snackbar.js", "/web_studio/static/src/client_action/report_editor/report_editor_wysiwyg/qweb_table_plugin.js", "/web_studio/static/src/client_action/report_editor/report_editor_wysiwyg/report_editor_wysiwyg.js", "/web_studio/static/src/client_action/report_editor/report_editor_wysiwyg/studio_dynamic_placeholder_popover.js", "/web_studio/static/src/client_action/report_editor/report_editor_xml/report_editor_xml.js", "/web_studio/static/src/client_action/report_editor/report_editor_xml/report_record_navigation.js", "/web_studio/static/src/client_action/report_editor/utils.js", "/web_studio/static/src/client_action/studio_client_action.js", "/web_studio/static/src/client_action/studio_home_menu/icon_creator_dialog/icon_creator_dialog.js", "/web_studio/static/src/client_action/studio_home_menu/studio_home_menu.js", "/web_studio/static/src/client_action/utils.js", "/web_studio/static/src/client_action/view_editor/default_view_sidebar/default_view_sidebar.js", "/web_studio/static/src/client_action/view_editor/editors/calendar/calendar_editor.js", "/web_studio/static/src/client_action/view_editor/editors/cohort/cohort_editor.js", "/web_studio/static/src/client_action/view_editor/editors/components/field_content_overlay.js", "/web_studio/static/src/client_action/view_editor/editors/components/field_selector_dialog.js", "/web_studio/static/src/client_action/view_editor/editors/components/field_studio.js", "/web_studio/static/src/client_action/view_editor/editors/components/studio_hook_component.js", "/web_studio/static/src/client_action/view_editor/editors/components/view_button_studio.js", "/web_studio/static/src/client_action/view_editor/editors/components/view_fields.js", "/web_studio/static/src/client_action/view_editor/editors/components/view_structures.js", "/web_studio/static/src/client_action/view_editor/editors/components/widget_studio.js", "/web_studio/static/src/client_action/view_editor/editors/form/chatter_container.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_compiler.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_controller/form_editor_controller.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_renderer/form_editor_groups.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_renderer/form_editor_renderer.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_renderer/form_editor_renderer_components.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/form_editor_sidebar.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/button_properties/button_properties.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/button_properties/new_button_box_dialog.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/button_properties/rainbow_effect.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/chatter_properties/chatter_properties.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/group_properties/group_properties.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/label_properties/label_properties.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/o_td_label_properties/o_td_label_properties.js", "/web_studio/static/src/client_action/view_editor/editors/form/form_editor_sidebar/properties/page_properties/page_properties.js", "/web_studio/static/src/client_action/view_editor/editors/gantt/gantt_editor_sidebar.js", "/web_studio/static/src/client_action/view_editor/editors/graph/graph_editor.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_compiler.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_record.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_renderer.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_sidebar/kanban_editor_sidebar.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_sidebar/properties/aside_properties/aside_properties.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_sidebar/properties/div_properties/div_properties.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_sidebar/properties/footer_properties/footer_properties.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_sidebar/properties/kanban_button_properties/kanban_button_properties.js", "/web_studio/static/src/client_action/view_editor/editors/kanban/kanban_editor_sidebar/properties/menu_properties/menu_properties.js", "/web_studio/static/src/client_action/view_editor/editors/kanban_legacy/kanban_editor_compiler_legacy.js", "/web_studio/static/src/client_action/view_editor/editors/kanban_legacy/kanban_editor_legacy.js", "/web_studio/static/src/client_action/view_editor/editors/kanban_legacy/kanban_editor_record_legacy.js", "/web_studio/static/src/client_action/view_editor/editors/kanban_legacy/kanban_editor_renderer_legacy.js", "/web_studio/static/src/client_action/view_editor/editors/kanban_legacy/kanban_editor_sidebar_legacy/kanban_editor_sidebar_legacy.js", "/web_studio/static/src/client_action/view_editor/editors/kanban_legacy/kanban_editor_sidebar_legacy/properties/kanban_cover_properties/kanban_cover_properties.js", "/web_studio/static/src/client_action/view_editor/editors/list/list_editor.js", "/web_studio/static/src/client_action/view_editor/editors/list/list_editor_renderer.js", "/web_studio/static/src/client_action/view_editor/editors/list/list_editor_sidebar/list_editor_sidebar.js", "/web_studio/static/src/client_action/view_editor/editors/map/map_editor_sidebar.js", "/web_studio/static/src/client_action/view_editor/editors/pivot/pivot_editor.js", "/web_studio/static/src/client_action/view_editor/editors/search/search_editor.js", "/web_studio/static/src/client_action/view_editor/editors/sidebar_safe_fields.js", "/web_studio/static/src/client_action/view_editor/editors/utils.js", "/web_studio/static/src/client_action/view_editor/editors/xml_utils.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/action_button/action_button.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/field_configuration/field_configuration.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/field_configuration/selection_content_dialog.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/interactive_editor.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/interactive_editor_sidebar.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/field_properties/field_properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/limit_group_visibility/limit_group_visibility.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/modifiers/modifiers_properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/type_widget_properties/type_specific_and_computed_properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/type_widget_properties/type_widget_properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/properties/widget_properties/widget_properties.js", "/web_studio/static/src/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox.js", "/web_studio/static/src/client_action/view_editor/operations_utils.js", "/web_studio/static/src/client_action/view_editor/property/property.js", "/web_studio/static/src/client_action/view_editor/studio_view.js", "/web_studio/static/src/client_action/view_editor/view_editor.js", "/web_studio/static/src/client_action/view_editor/view_editor_hook.js", "/web_studio/static/src/client_action/view_editor/view_editor_model.js", "/web_studio/static/src/client_action/view_editor/view_editor_snackbar.js", "/web_studio/static/src/client_action/xml_resource_editor/xml_resource_editor.js", "/web_studio/static/src/views/kanban_report/new_report_dialog.js", "/web_studio/static/src/views/kanban_report/report_kanban_view.js", "/website_studio/static/src/editor_tabs.js"], "mappings": "AAAA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC95BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzuBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtmCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9nCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzqCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpoBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9fA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9uBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7eA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzgBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5XA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC91BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { GROUPABLE_TYPES } from \"@web/search/utils/misc\";\n\nconst MODES = [\"bar\", \"line\", \"pie\"];\nconst ORDERS = [\"ASC\", \"DESC\", \"asc\", \"desc\", null];\n\nexport class GraphArchParser {\n    parse(arch, fields = {}) {\n        const archInfo = { fields, fieldAttrs: {}, groupBy: [], measures: [] };\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"graph\": {\n                    if (node.hasAttribute(\"disable_linking\")) {\n                        archInfo.disableLinking = exprToBoolean(\n                            node.getAttribute(\"disable_linking\")\n                        );\n                    }\n                    if (node.hasAttribute(\"stacked\")) {\n                        archInfo.stacked = exprToBoolean(node.getAttribute(\"stacked\"));\n                    }\n                    if (node.hasAttribute(\"cumulated\")) {\n                        archInfo.cumulated = exprToBoolean(node.getAttribute(\"cumulated\"));\n                    }\n                    if (node.hasAttribute(\"cumulated_start\")) {\n                        archInfo.cumulatedStart = exprToBoolean(\n                            node.getAttribute(\"cumulated_start\")\n                        );\n                    }\n                    const mode = node.getAttribute(\"type\");\n                    if (mode && MODES.includes(mode)) {\n                        archInfo.mode = mode;\n                    }\n                    const order = node.getAttribute(\"order\");\n                    if (order && ORDERS.includes(order)) {\n                        archInfo.order = order.toUpperCase();\n                    }\n                    const title = node.getAttribute(\"string\");\n                    if (title) {\n                        archInfo.title = title;\n                    }\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n                    if (fieldName === \"id\") {\n                        break;\n                    }\n                    const string = node.getAttribute(\"string\");\n                    if (string) {\n                        if (!archInfo.fieldAttrs[fieldName]) {\n                            archInfo.fieldAttrs[fieldName] = {};\n                        }\n                        archInfo.fieldAttrs[fieldName].string = string;\n                    }\n                    const widget = node.getAttribute(\"widget\");\n                    if (widget) {\n                        if (!archInfo.fieldAttrs[fieldName]) {\n                            archInfo.fieldAttrs[fieldName] = {};\n                        }\n                        archInfo.fieldAttrs[fieldName].widget = widget;\n                    }\n                    if (\n                        node.getAttribute(\"invisible\") === \"True\" ||\n                        node.getAttribute(\"invisible\") === \"1\"\n                    ) {\n                        if (!archInfo.fieldAttrs[fieldName]) {\n                            archInfo.fieldAttrs[fieldName] = {};\n                        }\n                        archInfo.fieldAttrs[fieldName].isInvisible = true;\n                        break;\n                    }\n                    const isMeasure = node.getAttribute(\"type\") === \"measure\";\n                    if (isMeasure) {\n                        archInfo.measures.push(fieldName);\n                        // the last field with type=\"measure\" (if any) will be used as measure else __count\n                        archInfo.measure = fieldName;\n                    } else {\n                        const { type } = archInfo.fields[fieldName]; // exists (rng validation)\n                        if (GROUPABLE_TYPES.includes(type)) {\n                            let groupBy = fieldName;\n                            const interval = node.getAttribute(\"interval\");\n                            if (interval) {\n                                groupBy += `:${interval}`;\n                            }\n                            archInfo.groupBy.push(groupBy);\n                        }\n                    }\n                    break;\n                }\n            }\n        });\n        return archInfo;\n    }\n}\n", "import { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class GraphController extends Component {\n    static template = \"web.GraphView\";\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);\n\n        useSetupAction({\n            rootRef: useRef(\"root\"),\n            getLocalState: () => {\n                return { metaData: this.model.metaData };\n            },\n            getContext: () => this.getContext(),\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    /**\n     * @returns {Object}\n     */\n    getContext() {\n        // expand context object? change keys?\n        const { measure, groupBy, mode } = this.model.metaData;\n        const context = {\n            graph_measure: measure,\n            graph_mode: mode,\n            graph_groupbys: groupBy.map((gb) => gb.spec),\n        };\n        if (mode !== \"pie\") {\n            context.graph_order = this.model.metaData.order;\n            context.graph_stacked = this.model.metaData.stacked;\n            if (mode === \"line\") {\n                context.graph_cumulated = this.model.metaData.cumulated;\n            }\n        }\n        return context;\n    }\n\n    loadAll() {\n        return this.model.forceLoadAll();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { sortBy, groupBy } from \"@web/core/utils/arrays\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { rankInterval } from \"@web/search/utils/dates\";\nimport { getGroupBy } from \"@web/search/utils/group_by\";\nimport { GROUPABLE_TYPES } from \"@web/search/utils/misc\";\nimport { addPropertyFieldDefs, Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\nimport { Domain } from \"@web/core/domain\";\n\nexport const SEP = \" / \";\nconst DATA_LIMIT = 80;\n\nexport const SEQUENTIAL_TYPES = [\"date\", \"datetime\"];\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nclass DateClasses {\n    // We view the param \"array\" as a matrix of values and undefined.\n    // An equivalence class is formed of defined values of a column.\n    // So nothing has to do with dates but we only use Dateclasses to manage\n    // identification of dates.\n    /**\n     * @param {(any[])[]} array\n     */\n    constructor(array) {\n        this.__referenceIndex = null;\n        this.__array = array;\n        for (let i = 0; i < this.__array.length; i++) {\n            const arr = this.__array[i];\n            if (arr.length && this.__referenceIndex === null) {\n                this.__referenceIndex = i;\n            }\n        }\n    }\n\n    /**\n     * @param {number} index\n     * @param {any} o\n     * @returns {string}\n     */\n    classLabel(index, o) {\n        return `${this.__array[index].indexOf(o)}`;\n    }\n\n    /**\n     * @param {string} classLabel\n     * @returns {any[]}\n     */\n    classMembers(classLabel) {\n        const classNumber = Number(classLabel);\n        const classMembers = new Set();\n        for (const arr of this.__array) {\n            if (arr[classNumber] !== undefined) {\n                classMembers.add(arr[classNumber]);\n            }\n        }\n        return [...classMembers];\n    }\n\n    /**\n     * @param {string} classLabel\n     * @param {number} [index]\n     * @returns {any}\n     */\n    representative(classLabel, index) {\n        const classNumber = Number(classLabel);\n        const i = index === undefined ? this.__referenceIndex : index;\n        if (i === null) {\n            return null;\n        }\n        return this.__array[i][classNumber];\n    }\n\n    /**\n     * @param {number} index\n     * @returns {number}\n     */\n    arrayLength(index) {\n        return this.__array[index].length;\n    }\n}\n\nexport class GraphModel extends Model {\n    /**\n     * @override\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _fetchDataPoints = this._fetchDataPoints.bind(this);\n        this._fetchDataPoints = (...args) => {\n            return this.race.add(_fetchDataPoints(...args));\n        };\n\n        this.initialGroupBy = null;\n\n        this.metaData = params;\n        this.data = null;\n        this.searchParams = null;\n        // This dataset will be added as a line plot on top of stacked bar chart.\n        this.lineOverlayDataset = null;\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n        if (!this.initialGroupBy) {\n            this.initialGroupBy = searchParams.context.graph_groupbys || this.metaData.groupBy; // = arch groupBy --> change that\n        }\n        const metaData = this._buildMetaData();\n        await addPropertyFieldDefs(\n            this.orm,\n            metaData.resModel,\n            searchParams.context,\n            metaData.fields,\n            metaData.groupBy.map((gb) => gb.fieldName)\n        );\n        await this._fetchDataPoints(metaData);\n    }\n\n    async forceLoadAll() {\n        const metaData = this._buildMetaData();\n        await this._fetchDataPoints(metaData, true);\n        this.notify();\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.dataPoints.length > 0;\n    }\n\n    /**\n     * Only supposed to be called to change one or several parameters among\n     * \"measure\", \"mode\", \"order\", \"stacked\" and \"cumulated\".\n     * @param {Object} params\n     */\n    async updateMetaData(params) {\n        if (\"measure\" in params) {\n            const metaData = this._buildMetaData(params);\n            await this._fetchDataPoints(metaData);\n            this.useSampleModel = false;\n        } else {\n            await this.race.getCurrentProm();\n            this.metaData = Object.assign({}, this.metaData, params);\n            this._prepareData();\n        }\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     * @param {Object} [params={}]\n     * @returns {Object}\n     */\n    _buildMetaData(params) {\n        const { comparison, domain, context, groupBy } = this.searchParams;\n\n        const metaData = Object.assign({}, this.metaData, { context });\n        if (comparison) {\n            metaData.domains = comparison.domains;\n            metaData.comparisonField = comparison.fieldName;\n        } else {\n            metaData.domains = [{ arrayRepr: domain, description: null }];\n        }\n        metaData.measure = context.graph_measure || metaData.measure;\n        metaData.mode = context.graph_mode || metaData.mode;\n        metaData.groupBy = groupBy.length ? groupBy : this.initialGroupBy;\n        if (metaData.mode !== \"pie\") {\n            metaData.order = \"graph_order\" in context ? context.graph_order : metaData.order;\n            if (comparison) {\n                metaData.stacked = false;\n            } else if (\"graph_stacked\" in context) {\n                metaData.stacked = context.graph_stacked;\n            }\n            if (metaData.mode === \"line\") {\n                metaData.cumulated =\n                    \"graph_cumulated\" in context ? context.graph_cumulated : metaData.cumulated;\n            }\n        }\n\n        this._normalize(metaData);\n\n        metaData.measures = computeReportMeasures(metaData.fields, metaData.fieldAttrs, [\n            ...(metaData.viewMeasures || []),\n            metaData.measure,\n        ]);\n\n        return Object.assign(metaData, params);\n    }\n\n    /**\n     * Fetch the data points determined by the metaData. This function has\n     * several side effects. It can alter this.metaData and set this.dataPoints.\n     * @protected\n     * @param {Object} metaData\n     * @param {boolean} [forceUseAllDataPoints=false]\n     */\n    async _fetchDataPoints(metaData, forceUseAllDataPoints = false) {\n        this.dataPoints = await this.keepLast.add(this._loadDataPoints(metaData));\n        this.metaData = metaData;\n        this._prepareData(forceUseAllDataPoints);\n    }\n\n    /**\n     * Separates dataPoints coming from the read_group(s) into different\n     * datasets. This function returns the parameters data and labels used\n     * to produce the charts.\n     * @protected\n     * @param {Object[]} dataPoints\n     * @param {boolean} forceUseAllDataPoints\n     * @returns {Object}\n     */\n    _getData(dataPoints, forceUseAllDataPoints) {\n        const { comparisonField, groupBy, mode } = this.metaData;\n\n        let identify = false;\n        if (comparisonField && groupBy.length && groupBy[0].fieldName === comparisonField) {\n            identify = true;\n        }\n        const dateClasses = identify ? this._getDateClasses(dataPoints) : null;\n\n        const dataPtMapping = new WeakMap();\n        const datasetsTmp = {};\n        let exceeds = false;\n\n        // dataPoints --> labels\n        let labels = [];\n        const labelMap = {};\n        for (const dataPt of dataPoints) {\n            const datasetLabel = this._getDatasetLabel(dataPt);\n            if (!(datasetLabel in datasetsTmp)) {\n                if (!forceUseAllDataPoints && Object.keys(datasetsTmp).length >= DATA_LIMIT) {\n                    exceeds = true;\n                    continue;\n                }\n                datasetsTmp[datasetLabel] = {\n                    label: datasetLabel,\n                    originIndex: dataPt.originIndex,\n                }; // add the entry but don't initialize it entirely\n            }\n            dataPtMapping.set(dataPt, datasetsTmp[datasetLabel]);\n\n            const x = dataPt.labels.slice(0, mode === \"pie\" ? undefined : 1);\n            const trueLabel = x.length ? x.join(SEP) : _t(\"Total\");\n            if (dateClasses) {\n                x[0] = dateClasses.classLabel(dataPt.originIndex, x[0]);\n            }\n            const key = JSON.stringify(x);\n            if (labelMap[key] === undefined) {\n                labelMap[key] = labels.length;\n                if (dateClasses) {\n                    if (mode === \"pie\") {\n                        x[0] = dateClasses.classMembers(x[0]).join(\", \");\n                    } else {\n                        x[0] = dateClasses.representative(x[0]);\n                    }\n                }\n                const label = x.length ? x.join(SEP) : _t(\"Total\");\n                labels.push(label);\n            }\n            dataPt.labelIndex = labelMap[key];\n            dataPt.trueLabel = trueLabel;\n        }\n\n        // dataPoints + labels --> datasetsTmp --> datasets\n        for (const dataPt of dataPoints) {\n            if (!dataPtMapping.has(dataPt)) {\n                continue;\n            }\n\n            const {\n                domain,\n                labelIndex,\n                originIndex,\n                trueLabel,\n                value,\n                identifier,\n                cumulatedStart,\n            } = dataPt;\n            const dataset = dataPtMapping.get(dataPt);\n            if (!dataset.data) {\n                let dataLength = labels.length;\n                if (mode !== \"pie\" && dateClasses) {\n                    dataLength = dateClasses.arrayLength(originIndex);\n                }\n                Object.assign(dataset, {\n                    data: new Array(dataLength).fill(0),\n                    cumulatedStart,\n                    trueLabels: labels.slice(0, dataLength), // should be good // check this in case identify = true\n                    domains: new Array(dataLength).fill([]),\n                    identifiers: new Set(),\n                });\n            }\n            dataset.data[labelIndex] = value;\n            dataset.domains[labelIndex] = domain;\n            dataset.trueLabels[labelIndex] = trueLabel;\n            dataset.identifiers.add(identifier);\n        }\n        // sort by origin\n        let datasets = sortBy(Object.values(datasetsTmp), \"originIndex\");\n\n        if (mode === \"pie\") {\n            // We kinda have a matrix. We remove the zero columns and rows. This is a global operation.\n            // That's why it cannot be done before.\n            datasets = datasets.filter((dataset) => dataset.data.some((v) => Boolean(v)));\n            const labelsToKeepIndexes = {};\n            labels.forEach((_, index) => {\n                if (datasets.some((dataset) => Boolean(dataset.data[index]))) {\n                    labelsToKeepIndexes[index] = true;\n                }\n            });\n            labels = labels.filter((_, index) => labelsToKeepIndexes[index]);\n            for (const dataset of datasets) {\n                dataset.data = dataset.data.filter((_, index) => labelsToKeepIndexes[index]);\n                dataset.domains = dataset.domains.filter((_, index) => labelsToKeepIndexes[index]);\n                dataset.trueLabels = dataset.trueLabels.filter(\n                    (_, index) => labelsToKeepIndexes[index]\n                );\n            }\n        }\n\n        return {\n            datasets,\n            labels,\n            exceeds,\n        };\n    }\n\n    _getLabel(description) {\n        if (!description) {\n            return _t(\"Sum\");\n        } else {\n            return _t(\"Sum (%s)\", description);\n        }\n    }\n\n    _getLineOverlayDataset() {\n        const { domains, stacked } = this.metaData;\n        const data = this.data;\n        let lineOverlayDataset = null;\n        if (stacked) {\n            const stacks = groupBy(data.datasets, (dataset) => dataset.originIndex);\n            if (Object.keys(stacks).length == 1) {\n                const [[originIndex, datasets]] = Object.entries(stacks);\n                if (datasets.length > 1) {\n                    const data = [];\n                    for (const dataset of datasets) {\n                        for (let i = 0; i < dataset.data.length; i++) {\n                            data[i] = (data[i] || 0) + dataset.data[i];\n                        }\n                    }\n                    lineOverlayDataset = {\n                        label: this._getLabel(domains[originIndex].description),\n                        data,\n                        trueLabels: datasets[0].trueLabels,\n                    };\n                }\n            }\n        }\n        return lineOverlayDataset;\n    }\n\n    /**\n     * Determines the dataset to which the data point belongs.\n     * @protected\n     * @param {Object} dataPoint\n     * @returns {string}\n     */\n    _getDatasetLabel(dataPoint) {\n        const { measure, measures, domains, mode } = this.metaData;\n        const { labels, originIndex } = dataPoint;\n        if (mode === \"pie\") {\n            return domains[originIndex].description || \"\";\n        }\n        // ([origin] + second to last groupBys) or measure\n        let datasetLabel = labels.slice(1).join(SEP);\n        if (domains.length > 1) {\n            datasetLabel =\n                domains[originIndex].description + (datasetLabel ? SEP + datasetLabel : \"\");\n        }\n        datasetLabel = datasetLabel || measures[measure].string;\n        return datasetLabel;\n    }\n\n    /**\n     * @protected\n     * @param {Object[]} dataPoints\n     * @returns {DateClasses}\n     */\n    _getDateClasses(dataPoints) {\n        const { domains } = this.metaData;\n        const dateSets = domains.map(() => new Set());\n        for (const { labels, originIndex } of dataPoints) {\n            const date = labels[0];\n            dateSets[originIndex].add(date);\n        }\n        const arrays = dateSets.map((dateSet) => [...dateSet]);\n        return new DateClasses(arrays);\n    }\n\n    /**\n     * @protected\n     * @returns {string}\n     */\n    _getDefaultFilterLabel(field) {\n        return _t(\"None\");\n    }\n\n    /**\n     * Eventually filters and sort data points.\n     * @protected\n     * @returns {Object[]}\n     */\n    _getProcessedDataPoints() {\n        const { domains, groupBy, mode, order } = this.metaData;\n        let processedDataPoints = [];\n        if (mode === \"line\") {\n            processedDataPoints = this.dataPoints.filter(\n                (dataPoint) => dataPoint.labels[0] !== this._getDefaultFilterLabel(groupBy[0])\n            );\n        } else if (mode === \"pie\") {\n            processedDataPoints = this.dataPoints.filter(\n                (dataPoint) => dataPoint.value > 0 && dataPoint.count !== 0\n            );\n        } else {\n            processedDataPoints = this.dataPoints.filter((dataPoint) => dataPoint.count !== 0);\n        }\n\n        if (order !== null && mode !== \"pie\" && domains.length === 1 && groupBy.length > 0) {\n            // group data by their x-axis value, and then sort datapoints\n            // based on the sum of values by group in ascending/descending order\n            const groupedDataPoints = {};\n            for (const dataPt of processedDataPoints) {\n                const key = dataPt.labels[0]; // = x-axis value under the current assumptions\n                if (!groupedDataPoints[key]) {\n                    groupedDataPoints[key] = [];\n                }\n                groupedDataPoints[key].push(dataPt);\n            }\n            const groups = Object.values(groupedDataPoints);\n            const groupTotal = (group) => group.reduce((sum, dataPt) => sum + dataPt.value, 0);\n            processedDataPoints = sortBy(groups, groupTotal, order.toLowerCase()).flat();\n        }\n\n        return processedDataPoints;\n    }\n\n    /**\n     * Fetch and process graph data.  It is basically a(some) read_group(s)\n     * with correct fields for each domain.  We have to do some light processing\n     * to separate date groups in the field list, because they can be defined\n     * with an aggregation function, such as my_date:week.\n     * @protected\n     * @param {Object} metaData\n     * @returns {Object[]}\n     */\n    async _loadDataPoints(metaData) {\n        const { measure, domains, fields, groupBy, resModel, cumulatedStart } = metaData;\n        const fieldName = groupBy[0]?.fieldName;\n        const sequential_field =\n            cumulatedStart && SEQUENTIAL_TYPES.includes(fields[fieldName]?.type) ? fieldName : null;\n        const sequential_spec = sequential_field && groupBy[0].spec;\n        const measures = [\"__count\"];\n        if (measure !== \"__count\") {\n            let { aggregator, type } = fields[measure];\n            if (type === \"many2one\") {\n                aggregator = \"count_distinct\";\n            }\n            if (aggregator === undefined) {\n                throw new Error(\n                    `No aggregate function has been provided for the measure '${measure}'`\n                );\n            }\n            measures.push(`${measure}:${aggregator}`);\n        }\n\n        const numbering = {}; // used to avoid ambiguity with many2one with values with same labels:\n        // for instance [1, \"ABC\"] [3, \"ABC\"] should be distinguished.\n\n        const proms = domains.map(async (domain, originIndex) => {\n            const data = await this.orm.webReadGroup(\n                resModel,\n                domain.arrayRepr,\n                measures,\n                groupBy.map((gb) => gb.spec),\n                {\n                    lazy: false, // what is this thing???\n                    context: { fill_temporal: true, ...this.searchParams.context },\n                }\n            );\n            let start = false;\n            if (\n                cumulatedStart &&\n                sequential_field &&\n                data.groups.length &&\n                domain.arrayRepr.some((leaf) => leaf.length === 3 && leaf[0] == sequential_field)\n            ) {\n                const first_date = data.groups[0].__range[sequential_spec].from;\n                const new_domain = Domain.combine(\n                    [\n                        new Domain([[sequential_field, \"<\", first_date]]),\n                        Domain.removeDomainLeaves(domain.arrayRepr, [sequential_field]),\n                    ],\n                    \"AND\"\n                ).toList();\n                start = await this.orm.webReadGroup(\n                    resModel,\n                    new_domain,\n                    measures,\n                    groupBy.filter((gb) => gb.fieldName != sequential_field).map((gb) => gb.spec),\n                    {\n                        lazy: false, // what is this thing???\n                        context: { ...this.searchParams.context },\n                    }\n                );\n            }\n            const dataPoints = [];\n            const cumulatedStartValue = {};\n            if (start) {\n                for (const group of start.groups) {\n                    const rawValues = [];\n                    for (const gb of groupBy.filter((gb) => gb.fieldName != sequential_field)) {\n                        rawValues.push({ [gb.spec]: group[gb.spec] });\n                    }\n                    cumulatedStartValue[JSON.stringify(rawValues)] = group[measure];\n                }\n            }\n            for (const group of data.groups) {\n                const { __domain, __count } = group;\n                const labels = [];\n                const rawValues = [];\n                for (const gb of groupBy) {\n                    let label;\n                    const val = group[gb.spec];\n                    rawValues.push({ [gb.spec]: val });\n                    const fieldName = gb.fieldName;\n                    const { type } = fields[fieldName];\n                    if (type === \"boolean\") {\n                        label = `${val}`; // toUpperCase?\n                    } else if (val === false) {\n                        label = this._getDefaultFilterLabel(gb);\n                    } else if ([\"many2many\", \"many2one\"].includes(type)) {\n                        const [id, name] = val;\n                        const key = JSON.stringify([fieldName, name]);\n                        if (!numbering[key]) {\n                            numbering[key] = {};\n                        }\n                        const numbers = numbering[key];\n                        if (!numbers[id]) {\n                            numbers[id] = Object.keys(numbers).length + 1;\n                        }\n                        const num = numbers[id];\n                        label = num === 1 ? name : `${name} (${num})`;\n                    } else if (type === \"selection\") {\n                        const selected = fields[fieldName].selection.find((s) => s[0] === val);\n                        label = selected[1];\n                    } else {\n                        label = val;\n                    }\n                    labels.push(label);\n                }\n\n                let value = group[measure];\n                if (value instanceof Array) {\n                    // case where measure is a many2one and is used as groupBy\n                    value = 1;\n                }\n                if (!Number.isInteger(value)) {\n                    metaData.allIntegers = false;\n                }\n                const group_id = JSON.stringify(rawValues.slice(1));\n                dataPoints.push({\n                    count: __count,\n                    domain: __domain,\n                    value,\n                    labels,\n                    originIndex,\n                    identifier: JSON.stringify(rawValues),\n                    cumulatedStart: cumulatedStartValue[group_id] || 0,\n                });\n            }\n            return dataPoints;\n        });\n        const promResults = await Promise.all(proms);\n        return promResults.flat();\n    }\n\n    /**\n     * Process metaData.groupBy in order to keep only the finest interval option for\n     * elements based on date/datetime field (e.g. 'date:year'). This means that\n     * 'week' is prefered to 'month'. The field stays at the place of its first occurence.\n     * For instance,\n     * ['foo', 'date:month', 'bar', 'date:week'] becomes ['foo', 'date:week', 'bar'].\n     * @protected\n     * @param {Object} metaData\n     */\n    _normalize(metaData) {\n        const { fields } = metaData;\n        const groupBy = [];\n        for (const gb of metaData.groupBy) {\n            let ngb = gb;\n            if (typeof gb === \"string\") {\n                ngb = getGroupBy(gb, fields);\n            }\n            groupBy.push(ngb);\n        }\n\n        const processedGroupBy = [];\n        for (const gb of groupBy) {\n            const { fieldName, interval } = gb;\n            if (!fieldName.includes(\".\")) {\n                const { groupable, type } = fields[fieldName];\n                if (\n                    // cf. _description_groupable in odoo/fields.py\n                    !groupable ||\n                    [\"id\", \"__count\"].includes(fieldName) ||\n                    !GROUPABLE_TYPES.includes(type)\n                ) {\n                    continue;\n                }\n            }\n            const index = processedGroupBy.findIndex((gb) => gb.fieldName === fieldName);\n            if (index === -1) {\n                processedGroupBy.push(gb);\n            } else if (interval) {\n                const registeredInterval = processedGroupBy[index].interval;\n                if (rankInterval(registeredInterval) < rankInterval(interval)) {\n                    processedGroupBy.splice(index, 1, gb);\n                }\n            }\n        }\n        metaData.groupBy = processedGroupBy;\n\n        metaData.measure = processMeasure(metaData.measure);\n    }\n\n    /**\n     * @protected\n     * @param {boolean} [forceUseAllDataPoints=false]\n     */\n    _prepareData(forceUseAllDataPoints = false) {\n        const processedDataPoints = this._getProcessedDataPoints();\n        this.data = this._getData(processedDataPoints, forceUseAllDataPoints);\n        this.lineOverlayDataset = null;\n        if (this.metaData.mode === \"bar\") {\n            this.lineOverlayDataset = this._getLineOverlayDataset();\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    getBorderWhite,\n    DEFAULT_BG,\n    getColor,\n    getCustomColor,\n    lightenColor,\n    darkenColor,\n} from \"@web/core/colors/colors\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { SEP } from \"./graph_model\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillUnmount, useEffect, useRef, onWillStart } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { ReportViewMeasures } from \"@web/views/view_components/report_view_measures\";\n\nconst NO_DATA = _t(\"No data\");\nconst formatters = registry.category(\"formatters\");\n\nconst colorScheme = cookie.get(\"color_scheme\");\nconst GRAPH_LEGEND_COLOR = getCustomColor(colorScheme, \"#111827\", \"#ffffff\");\nconst GRAPH_GRID_COLOR = getCustomColor(colorScheme, \"rgba(0,0,0,.1)\", \"rgba(255,255,255,.15\");\nconst GRAPH_LABEL_COLOR = getCustomColor(colorScheme, \"#111827\", \"#E4E4E4\");\nconst NO_DATA_COLOR = getCustomColor(colorScheme, DEFAULT_BG, \"#3C3E4B\");\n\n/**\n * Custom Plugin for Line chart:\n * Draw the scale grid on top of the chart to\n * see this last one correctly.\n */\nconst gridOnTop = {\n    id: \"gridOnTop\",\n    afterDraw: (chart) => {\n        const elements = chart.getDatasetMeta(0).data || [];\n        const ctx = chart.ctx;\n        const chartArea = chart.chartArea;\n        const yAxis = chart.scales.y;\n        const xAxis = chart.scales.x;\n\n        ctx.lineWidth = 1;\n        ctx.strokeStyle = GRAPH_GRID_COLOR;\n\n        // Draw Y axis scale\n        yAxis.ticks.forEach((value, index) => {\n            const y = yAxis.getPixelForTick(index);\n            ctx.beginPath();\n            // Draw the line scale\n            ctx.moveTo(chartArea.left, y);\n            ctx.lineTo(chartArea.right, y);\n            // Draw the tick mark\n            ctx.moveTo(chartArea.left - 8, y);\n            ctx.lineTo(chartArea.left, y);\n            ctx.setLineDash([]);\n            ctx.stroke();\n        });\n\n        // Draw X axis tick marks\n        xAxis.ticks.forEach((value, tickIndex) => {\n            const x = xAxis.getPixelForTick(tickIndex);\n            ctx.beginPath();\n            ctx.moveTo(x, chartArea.bottom);\n            ctx.lineTo(x, chartArea.bottom + 8);\n            ctx.stroke();\n        });\n\n        // Draw the X axis dashed line\n        elements.forEach((point, eltIndex) => {\n            xAxis.ticks.forEach((value, tickIndex) => {\n                if (point.active && eltIndex === tickIndex) {\n                    const x = xAxis.getPixelForTick(tickIndex);\n                    ctx.beginPath();\n                    ctx.moveTo(x, chartArea.top);\n                    ctx.lineTo(x, chartArea.bottom);\n                    ctx.strokeStyle = GRAPH_GRID_COLOR;\n                    ctx.stroke();\n                }\n            });\n        });\n    },\n};\n\n/**\n * @param {Object} chartArea\n * @returns {string}\n */\nfunction getMaxWidth(chartArea) {\n    const { left, right } = chartArea;\n    return Math.floor((right - left) / 1.618) + \"px\";\n}\n\n/**\n * Used to avoid too long legend items.\n * @param {string} label\n * @returns {string} shortened version of the input label\n */\nfunction shortenLabel(label) {\n    // string returned could be wrong if a groupby value contain a \" / \"!\n    const groups = label.toString().split(SEP);\n    let shortLabel = groups.slice(0, 3).join(SEP);\n    if (shortLabel.length > 30) {\n        shortLabel = `${shortLabel.slice(0, 30)}...`;\n    } else if (groups.length > 3) {\n        shortLabel = `${shortLabel}${SEP}...`;\n    }\n    return shortLabel;\n}\n\nexport class GraphRenderer extends Component {\n    static template = \"web.GraphRenderer\";\n    static components = { Dropdown, DropdownItem, ReportViewMeasures };\n    static props = [\"class?\", \"model\", \"buttonTemplate\"];\n\n    setup() {\n        this.model = this.props.model;\n\n        this.rootRef = useRef(\"root\");\n        this.canvasRef = useRef(\"canvas\");\n        this.containerRef = useRef(\"container\");\n        this.actionService = useService(\"action\");\n\n        this.chart = null;\n        this.tooltip = null;\n        this.legendTooltip = null;\n\n        onWillStart(async () => {\n            await loadBundle(\"web.chartjs_lib\");\n        });\n\n        useEffect(() => this.renderChart());\n        onWillUnmount(this.onWillUnmount);\n    }\n\n    onWillUnmount() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n    }\n\n    /**\n     * This function aims to remove a suitable number of lines from the\n     * tooltip in order to make it reasonably visible. A message indicating\n     * the number of lines is added if necessary.\n     * @param {HTMLElement} tooltip\n     * @param {number} maxTooltipHeight this the max height in pixels of the tooltip\n     */\n    adjustTooltipHeight(tooltip, maxTooltipHeight) {\n        const sizeOneLine = tooltip.querySelector(\"tbody tr\").clientHeight;\n        const tbodySize = tooltip.querySelector(\"tbody\").clientHeight;\n        const toKeep = Math.max(\n            0,\n            Math.floor((maxTooltipHeight - (tooltip.clientHeight - tbodySize)) / sizeOneLine) - 1\n        );\n        const lines = tooltip.querySelectorAll(\"tbody tr\");\n        const toRemove = lines.length - toKeep;\n        if (toRemove > 0) {\n            for (let index = toKeep; index < lines.length; ++index) {\n                lines[index].remove();\n            }\n            const tr = document.createElement(\"tr\");\n            const td = document.createElement(\"td\");\n            tr.classList.add(\"o_show_more\", \"text-center\", \"fw-bold\");\n            td.setAttribute(\"colspan\", \"2\");\n            td.innerText = _t(\"...\");\n            tr.appendChild(td);\n            tooltip.querySelector(\"tbody\").appendChild(tr);\n        }\n    }\n\n    /**\n     * Creates a custom HTML tooltip.\n     * @param {Object} data\n     * @param {Object} metaData\n     * @param {Object} context see chartjs documentation\n     */\n    customTooltip(data, metaData, context) {\n        const tooltipModel = context.tooltip;\n        const { measure, measures, disableLinking, mode } = metaData;\n        this.containerRef.el.style.cursor = \"\";\n        this.removeTooltips();\n        if (tooltipModel.opacity === 0 || tooltipModel.dataPoints.length === 0) {\n            return;\n        }\n        if (!disableLinking && mode !== \"line\") {\n            this.containerRef.el.style.cursor = \"pointer\";\n        }\n        const chartAreaTop = this.chart.chartArea.top;\n        const viewContentTop = this.containerRef.el.getBoundingClientRect().top;\n        const innerHTML = renderToString(\"web.GraphRenderer.CustomTooltip\", {\n            maxWidth: getMaxWidth(this.chart.chartArea),\n            measure: measures[measure].string,\n            mode: this.model.metaData.mode,\n            tooltipItems: this.getTooltipItems(data, metaData, tooltipModel),\n        });\n        const template = Object.assign(document.createElement(\"template\"), { innerHTML });\n        const tooltip = template.content.firstChild;\n        this.containerRef.el.prepend(tooltip);\n\n        let top;\n        const tooltipHeight = tooltip.clientHeight;\n        const minTopAllowed = Math.floor(chartAreaTop);\n        const maxTopAllowed = Math.floor(window.innerHeight - (viewContentTop + tooltipHeight)) - 2;\n        const y = Math.floor(tooltipModel.y);\n        if (minTopAllowed <= maxTopAllowed) {\n            // Here we know that the full tooltip can fit in the screen.\n            // We put it in the position where Chart.js would put it\n            // if two conditions are respected:\n            //  1: the tooltip is not cut (because we know it is possible to not cut it)\n            //  2: the tooltip does not hide the legend.\n            // If it is not possible to use the Chart.js proposition (y)\n            // we use the best approximated value.\n            if (y <= maxTopAllowed) {\n                if (y >= minTopAllowed) {\n                    top = y;\n                } else {\n                    top = minTopAllowed;\n                }\n            } else {\n                top = maxTopAllowed;\n            }\n        } else {\n            // Here we know that we cannot satisfy condition 1 above,\n            // so we position the tooltip at the minimal position and\n            // cut it the minimum possible.\n            top = minTopAllowed;\n            const maxTooltipHeight = window.innerHeight - (viewContentTop + chartAreaTop) - 2;\n            this.adjustTooltipHeight(tooltip, maxTooltipHeight);\n        }\n        this.fixTooltipLeftPosition(tooltip, tooltipModel.x);\n        tooltip.style.top = Math.floor(top) + \"px\";\n\n        this.tooltip = tooltip;\n    }\n\n    /**\n     * Sets best left position of a tooltip approaching the proposal x.\n     * @param {HTMLElement} tooltip\n     * @param {number} x\n     */\n    fixTooltipLeftPosition(tooltip, x) {\n        let left;\n        const tooltipWidth = tooltip.clientWidth;\n        const minLeftAllowed = Math.floor(this.chart.chartArea.left + 2);\n        const maxLeftAllowed = Math.floor(this.chart.chartArea.right - tooltipWidth - 2);\n        x = Math.floor(x);\n        if (x < minLeftAllowed) {\n            left = minLeftAllowed;\n        } else if (x > maxLeftAllowed) {\n            left = maxLeftAllowed;\n        } else {\n            left = x;\n        }\n        tooltip.style.left = `${left}px`;\n    }\n\n    /**\n     * Used to format correctly the values in tooltip and y.\n     * @param {number} value\n     * @param {boolean} [allIntegers=true]\n     * @returns {string}\n     */\n    formatValue(value, allIntegers = true, formatType = \"\") {\n        const largeNumber = Math.abs(value) >= 1000;\n        if (formatType) {\n            return formatters.get(formatType)(value);\n        }\n        if (allIntegers && !largeNumber) {\n            return String(value);\n        }\n        if (largeNumber) {\n            return formatFloat(value, { humanReadable: true, decimals: 2, minDigits: 1 });\n        }\n        return formatFloat(value);\n    }\n\n    /**\n     * Returns the bar chart data\n     * @returns {Object}\n     */\n    getBarChartData() {\n        // style data\n        const { domains, stacked } = this.model.metaData;\n        const { data, lineOverlayDataset } = this.model;\n        for (let index = 0; index < data.datasets.length; ++index) {\n            const dataset = data.datasets[index];\n            const itemColor = getColor(index, colorScheme, data.datasets.length);\n            // used when stacked\n            if (stacked) {\n                dataset.stack = domains[dataset.originIndex].description || \"\";\n            }\n            // set dataset color\n            dataset.backgroundColor = itemColor;\n            dataset.borderRadius = 4;\n        }\n        if (lineOverlayDataset) {\n            // Mutate the lineOverlayDataset to include the config on how it will be displayed.\n            Object.assign(lineOverlayDataset, {\n                type: \"line\",\n                order: -1,\n                tension: 0,\n                fill: false,\n                pointHitRadius: 20,\n                pointRadius: 5,\n                pointHoverRadius: 10,\n                backgroundColor: getCustomColor(colorScheme, \"#343a40\", \"#e9ecef\"),\n                borderColor: getCustomColor(colorScheme, \"rgba(0,0,0,.3)\", \"rgba(255,255,255,.5)\"),\n                borderWidth: 2,\n                lineWidth: 3,\n            });\n            // We're not mutating the original datasets (`this.model.data.datasets`)\n            // because some part of the code depends on it.\n            return {\n                ...data,\n                datasets: [...data.datasets, lineOverlayDataset],\n            };\n        }\n\n        return data;\n    }\n\n    /**\n     * Returns the chart config.\n     * @returns {Object}\n     */\n    getChartConfig() {\n        const { mode } = this.model.metaData;\n        let data;\n        switch (mode) {\n            case \"bar\":\n                data = this.getBarChartData();\n                break;\n            case \"line\":\n                data = this.getLineChartData();\n                break;\n            case \"pie\":\n                data = this.getPieChartData();\n        }\n        const options = this.prepareOptions();\n        const config = { data, options, type: mode };\n        if (mode === \"line\") {\n            config.plugins = [gridOnTop];\n        }\n        return config;\n    }\n\n    /**\n     * Returns the animation options.\n     * 1. This adds progressive animation for Bar & Line charts.\n     * 2. Reduce animation duration for Pie chart.\n     * @returns {Object}\n     */\n    getAnimationOptions() {\n        let delayed;\n        const { mode } = this.model.metaData;\n        const labelsCount = this.model.data.labels.length;\n        const gap = 350;\n        const animationOptions = {};\n        if (mode === \"pie\") {\n            animationOptions.offset = { duration: 200 };\n        } else {\n            animationOptions.duration = 600;\n            animationOptions.onComplete = () => {\n                delayed = true;\n            };\n            animationOptions.delay = (context) => {\n                let delay = 0;\n                if ((mode === \"bar\" || mode === \"line\") && !delayed) {\n                    delay = context.dataIndex * (gap / labelsCount);\n                }\n                return delay;\n            };\n        }\n        return animationOptions;\n    }\n\n    /**\n     * Returns an object used to style chart elements independently from\n     * the datasets.\n     * @returns {Object}\n     */\n    getElementOptions() {\n        const { mode, stacked } = this.model.metaData;\n        const elementOptions = {};\n        if (mode === \"bar\") {\n            elementOptions.bar = { borderWidth: 1 };\n        } else if (mode === \"line\") {\n            elementOptions.line = { fill: stacked, tension: 0 };\n        }\n        return elementOptions;\n    }\n\n    /**\n     * @returns {Object}\n     */\n    getLegendOptions() {\n        const { mode } = this.model.metaData;\n        const legendOptions = {\n            onHover: this.onLegendHover.bind(this),\n            onLeave: this.onLegendLeave.bind(this),\n        };\n        if (mode === \"line\") {\n            legendOptions.onClick = this.onLegendClick.bind(this);\n        }\n        if (mode === \"pie\") {\n            legendOptions.labels = {\n                generateLabels: (chart) => {\n                    return chart.data.labels.map((label, index) => {\n                        const hidden = !chart.getDataVisibility(index);\n                        const fullText = label;\n                        const text = shortenLabel(fullText);\n                        const fillStyle =\n                            label === NO_DATA\n                                ? NO_DATA_COLOR\n                                : getColor(index, colorScheme, chart.data.labels.length);\n                        return {\n                            text,\n                            fullText,\n                            fillStyle,\n                            hidden,\n                            index,\n                            fontColor: GRAPH_LEGEND_COLOR,\n                            lineWidth: 0,\n                        };\n                    });\n                },\n            };\n        } else {\n            legendOptions.position = \"top\";\n            legendOptions.align = \"end\";\n            const referenceColor = mode === \"bar\" ? \"backgroundColor\" : \"borderColor\";\n            legendOptions.labels = {\n                generateLabels: (chart) => {\n                    const { data } = chart;\n                    const labels = data.datasets.map((dataset, index) => {\n                        return {\n                            text: shortenLabel(dataset.label),\n                            fullText: dataset.label,\n                            fillStyle: dataset[referenceColor],\n                            hidden: !chart.isDatasetVisible(index),\n                            lineCap: dataset.borderCapStyle,\n                            lineDash: dataset.borderDash,\n                            lineDashOffset: dataset.borderDashOffset,\n                            lineJoin: dataset.borderJoinStyle,\n                            lineWidth: dataset.borderWidth,\n                            strokeStyle: dataset[referenceColor],\n                            pointStyle: dataset.pointStyle,\n                            datasetIndex: index,\n                            fontColor: GRAPH_LEGEND_COLOR,\n                        };\n                    });\n                    return labels;\n                },\n            };\n        }\n        return legendOptions;\n    }\n\n    /**\n     * Returns line chart data.\n     * @returns {Object}\n     */\n    getLineChartData() {\n        const { cumulated } = this.model.metaData;\n        const data = this.model.data;\n        for (let index = 0; index < data.datasets.length; ++index) {\n            const dataset = data.datasets[index];\n            const itemColor = getColor(index, colorScheme, data.datasets.length);\n            dataset.backgroundColor = getCustomColor(\n                colorScheme,\n                lightenColor(itemColor, 0.5),\n                darkenColor(itemColor, 0.5)\n            );\n            dataset.cubicInterpolationMode = \"monotone\";\n            dataset.borderColor = itemColor;\n            dataset.borderWidth = 2;\n            dataset.hoverBackgroundColor = dataset.borderColor;\n            dataset.pointRadius = 3;\n            dataset.pointHoverRadius = 6;\n            if (cumulated) {\n                let accumulator = dataset.cumulatedStart;\n                dataset.data = dataset.data.map((value) => {\n                    accumulator += value;\n                    return accumulator;\n                });\n            }\n            if (data.labels.length === 1) {\n                // shift of the real value to right. This is done to\n                // center the points in the chart. See data.labels below in\n                // Chart parameters\n                dataset.data.unshift(undefined);\n                dataset.trueLabels.unshift(undefined);\n                dataset.domains.unshift(undefined);\n            }\n            dataset.pointBackgroundColor = dataset.borderColor;\n        }\n        // center the points in the chart (without that code they are put\n        // on the left and the graph seems empty)\n        data.labels = data.labels.length > 1 ? data.labels : [\"\", ...data.labels, \"\"];\n        return data;\n    }\n\n    /**\n     * Returns pie chart data.\n     * @returns {Object}\n     */\n    getPieChartData() {\n        const { domains } = this.model.metaData;\n        const data = this.model.data;\n        // style/complete data\n        // give same color to same groups from different origins\n        const colors = data.labels.map((_, index) =>\n            getColor(index, colorScheme, data.labels.length)\n        );\n        const borderColor = getBorderWhite(colorScheme);\n        for (const dataset of data.datasets) {\n            dataset.backgroundColor = colors;\n            dataset.hoverBackgroundColor = colors;\n            dataset.borderColor = borderColor;\n            dataset.hoverOffset = 60;\n        }\n        // make sure there is a zone associated with every origin\n        const representedOriginIndexes = new Set(\n            data.datasets.map((dataset) => dataset.originIndex)\n        );\n        let addNoDataToLegend = false;\n        const fakeData = new Array(data.labels.length + 1);\n        fakeData[data.labels.length] = 1;\n        const fakeTrueLabels = new Array(data.labels.length + 1);\n        fakeTrueLabels[data.labels.length] = NO_DATA;\n        for (let index = 0; index < domains.length; ++index) {\n            if (!representedOriginIndexes.has(index)) {\n                data.datasets.push({\n                    label: domains[index].description,\n                    data: fakeData,\n                    trueLabels: fakeTrueLabels,\n                    backgroundColor: [...colors, NO_DATA_COLOR],\n                    borderColor,\n                });\n                addNoDataToLegend = true;\n            }\n        }\n        if (addNoDataToLegend) {\n            data.labels.push(NO_DATA);\n        }\n\n        return data;\n    }\n\n    /**\n     * Returns the options used to generate the chart axes.\n     * @returns {Object}\n     */\n    getScaleOptions() {\n        const { labels } = this.model.data;\n        const { fieldAttrs, measure, measures, mode, stacked } = this.model.metaData;\n        if (mode === \"pie\") {\n            return {};\n        }\n        const xAxe = {\n            type: \"category\",\n            ticks: {\n                callback: (val, index) => {\n                    const value = labels[index];\n                    return shortenLabel(value);\n                },\n                color: GRAPH_LABEL_COLOR,\n            },\n            grid: {\n                color: \"transparent\",\n            },\n            border: {\n                display: false,\n            },\n        };\n        const yAxe = {\n            beginAtZero: true,\n            type: \"linear\",\n            title: {\n                text: measures[measure].string,\n                color:\n                    cookie.get(\"color_scheme\") === \"dark\"\n                        ? getColor(15, cookie.get(\"color_scheme\"))\n                        : null,\n            },\n            ticks: {\n                callback: (value) => this.formatValue(value, false, fieldAttrs[measure]?.widget),\n                color: GRAPH_LABEL_COLOR,\n            },\n            stacked: mode === \"line\" && stacked ? stacked : undefined,\n            grid: {\n                display: mode === \"line\" ? false : true,\n                color: GRAPH_GRID_COLOR,\n            },\n            border: {\n                display: false,\n            },\n            suggestedMax: 0,\n            suggestedMin: 0,\n        };\n        return { x: xAxe, y: yAxe };\n    }\n\n    /**\n     * This function extracts the information from the data points in\n     * tooltipModel.dataPoints (corresponding to datapoints over a given\n     * label determined by the mouse position) that will be displayed in a\n     * custom tooltip.\n     * @param {Object} data\n     * @param {Object} metaData\n     * @param {Object} tooltipModel see chartjs documentation\n     * @returns {Object[]}\n     */\n    getTooltipItems(data, metaData, tooltipModel) {\n        const { allIntegers, domains, mode, groupBy, measure } = metaData;\n        const sortedDataPoints = sortBy(tooltipModel.dataPoints, \"raw\", \"desc\");\n        const items = [];\n        for (const item of sortedDataPoints) {\n            const index = item.dataIndex;\n            // If `datasetIndex` is not found in the `datasets`, then it refers to the `lineOverlayDataset`.\n            const dataset = data.datasets[item.datasetIndex] || this.model.lineOverlayDataset;\n            let label = dataset.trueLabels[index];\n            let value = dataset.data[index];\n            const measureWidget = metaData.fieldAttrs[measure]?.widget;\n            value = this.formatValue(value, allIntegers, measureWidget);\n            let boxColor;\n            let percentage;\n            if (mode === \"pie\") {\n                if (label === NO_DATA) {\n                    value = this.formatValue(0, allIntegers, measureWidget);\n                }\n                if (domains.length > 1) {\n                    label = `${dataset.label} / ${label}`;\n                }\n                boxColor = dataset.backgroundColor[index];\n                const totalData = dataset.data.reduce((a, b) => a + b, 0);\n                percentage = totalData && ((dataset.data[index] * 100) / totalData).toFixed(2);\n            } else {\n                if (groupBy.length > 1 || domains.length > 1) {\n                    label = `${label} / ${dataset.label}`;\n                }\n                boxColor = mode === \"bar\" ? dataset.backgroundColor : dataset.borderColor;\n            }\n            items.push({ label, value, boxColor, percentage });\n        }\n        return items;\n    }\n\n    /**\n     * Returns the options used to generate chart tooltips.\n     * @returns {Object}\n     */\n    getTooltipOptions() {\n        const { data, metaData } = this.model;\n        const { mode } = metaData;\n        const tooltipOptions = {\n            enabled: false,\n            external: this.customTooltip.bind(this, data, metaData),\n        };\n        if (mode === \"line\") {\n            tooltipOptions.mode = \"index\";\n            tooltipOptions.intersect = false;\n            tooltipOptions.position = \"average\";\n        }\n        if (mode === \"bar\") {\n            tooltipOptions.xAlign = \"center\";\n            tooltipOptions.yAlign = \"bottom\";\n        }\n        if (mode === \"pie\") {\n            tooltipOptions.xAlign = \"center\";\n            tooltipOptions.yAlign = \"center\";\n        }\n        return tooltipOptions;\n    }\n\n    /**\n     * If a group has been clicked on, display a view of its records.\n     * @param {MouseEvent} ev\n     */\n    onGraphClicked(ev) {\n        const [activeElement] = this.chart.getElementsAtEventForMode(\n            ev,\n            \"nearest\",\n            { intersect: true },\n            false\n        );\n        if (!activeElement) {\n            return;\n        }\n        const { datasetIndex, index } = activeElement;\n        const { domains } = this.chart.data.datasets[datasetIndex];\n        if (domains) {\n            this.onGraphClickedFinal(domains[index]);\n        }\n    }\n\n    /**\n     * Overrides the default legend 'onClick' behaviour. This is done to\n     * remove all existing tooltips right before updating the chart.\n     * @param {Event} ev\n     * @param {Object} legendItem\n     */\n    onLegendClick(ev, legendItem) {\n        this.removeTooltips();\n        // Default 'onClick' fallback. See web/static/lib/Chart/Chart.js#15138\n        const index = legendItem.datasetIndex;\n        const meta = this.chart.getDatasetMeta(index);\n        meta.hidden = meta.hidden === null ? !this.chart.data.datasets[index].hidden : null;\n        this.chart.update();\n    }\n\n    /**\n     * If the text of a legend item has been shortened and the user mouse\n     * hovers that item (actually the event type is mousemove), a tooltip\n     * with the item full text is displayed.\n     * @param {Event} ev\n     * @param {Object} legendItem\n     */\n    onLegendHover(ev, legendItem) {\n        ev = ev.native;\n        this.canvasRef.el.style.cursor = \"pointer\";\n        /**\n         * The string legendItem.text is an initial segment of legendItem.fullText.\n         * If the two coincide, no need to generate a tooltip. If a tooltip\n         * for the legend already exists, it is already good and does not\n         * need to be recreated.\n         */\n        const { fullText, text } = legendItem;\n        if (this.legendTooltip || text === fullText) {\n            return;\n        }\n        const viewContentTop = this.canvasRef.el.getBoundingClientRect().top;\n        const legendTooltip = Object.assign(document.createElement(\"div\"), {\n            className: \"o_tooltip_legend popover p-3 pe-none position-absolute\",\n            innerText: fullText,\n        });\n        legendTooltip.style.top = `${ev.clientY - viewContentTop}px`;\n        legendTooltip.style.maxWidth = getMaxWidth(this.chart.chartArea);\n        this.containerRef.el.appendChild(legendTooltip);\n        this.fixTooltipLeftPosition(legendTooltip, ev.clientX);\n        this.legendTooltip = legendTooltip;\n    }\n\n    /**\n     * If there's a legend tooltip and the user mouse out of the\n     * corresponding legend item, the tooltip is removed.\n     */\n    onLegendLeave() {\n        this.canvasRef.el.style.cursor = \"\";\n        this.removeLegendTooltip();\n    }\n\n    /**\n     * Prepares options for the chart according to the current mode\n     * (= chart type). This function returns the parameter options used to\n     * instantiate the chart.\n     */\n    prepareOptions() {\n        const { disableLinking, mode } = this.model.metaData;\n        const options = {\n            maintainAspectRatio: false,\n            scales: this.getScaleOptions(),\n            plugins: {\n                legend: this.getLegendOptions(),\n                tooltip: this.getTooltipOptions(),\n            },\n            elements: this.getElementOptions(),\n            onResize: () => {\n                this.resizeChart(options);\n            },\n            animation: this.getAnimationOptions(),\n        };\n        if (!disableLinking && mode !== \"line\") {\n            options.onClick = this.onGraphClicked.bind(this);\n        }\n        if (mode === \"line\") {\n            options.interaction = {\n                mode: \"index\",\n                intersect: false,\n            };\n        }\n        if (mode === \"pie\") {\n            options.radius = \"90%\";\n        }\n        return options;\n    }\n\n    /**\n     * Adapt Pie chart layout on mobile\n     * @param {Object} context\n     */\n    resizeChart(context) {\n        const { mode } = this.model.metaData;\n        if (mode === \"pie\") {\n            if (this.env.isSmall) {\n                context.plugins.legend.position = \"bottom\";\n                context.plugins.legend.align = \"center\";\n            } else {\n                context.plugins.legend.position = \"right\";\n                context.plugins.legend.align = \"start\";\n            }\n        }\n    }\n\n    /**\n     * Removes the legend tooltip (if any).\n     */\n    removeLegendTooltip() {\n        if (this.legendTooltip) {\n            this.legendTooltip.remove();\n            this.legendTooltip = null;\n        }\n    }\n\n    /**\n     * Removes all existing tooltips (if any).\n     */\n    removeTooltips() {\n        if (this.tooltip) {\n            this.tooltip.remove();\n            this.tooltip = null;\n        }\n        this.removeLegendTooltip();\n    }\n\n    /**\n     * Instantiates a Chart (Chart.js lib) to render the graph according to\n     * the current config.\n     */\n    renderChart() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n        if (this.canvasRef.el) {\n            const config = this.getChartConfig();\n            this.chart = new Chart(this.canvasRef.el, config);\n        }\n    }\n\n    /**\n     * Execute the action to open the view on the current model.\n     *\n     * @param {Array} domain\n     * @param {Array} views\n     * @param {Object} context\n     */\n    openView(domain, views, context) {\n        this.actionService.doAction(\n            {\n                context,\n                domain,\n                name: this.model.metaData.title,\n                res_model: this.model.metaData.resModel,\n                target: \"current\",\n                type: \"ir.actions.act_window\",\n                views,\n            },\n            {\n                viewType: \"list\",\n            }\n        );\n    }\n    /**\n     * @param {string} domain the domain of the clicked area\n     */\n    onGraphClickedFinal(domain) {\n        const { context } = this.model.metaData;\n\n        Object.keys(context).forEach((x) => {\n            if (x === \"group_by\" || x.startsWith(\"search_default_\")) {\n                delete context[x];\n            }\n        });\n\n        const views = {};\n        for (const [viewId, viewType] of this.env.config.views || []) {\n            views[viewType] = viewId;\n        }\n        function getView(viewType) {\n            return [views[viewType] || false, viewType];\n        }\n        const actionViews = [getView(\"list\"), getView(\"form\")];\n        this.openView(domain, actionViews, context);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {string} param0.measure\n     */\n    onMeasureSelected({ measure }) {\n        this.model.updateMetaData({ measure });\n    }\n\n    /**\n     * @param {\"bar\"|\"line\"|\"pie\"} mode\n     */\n    onModeSelected(mode) {\n        if (this.model.metaData.mode != mode) {\n            this.model.updateMetaData({ mode });\n        }\n    }\n\n    /**\n     * @param {\"ASC\"|\"DESC\"} order\n     */\n    toggleOrder(order) {\n        const { order: currentOrder } = this.model.metaData;\n        const nextOrder = currentOrder === order ? null : order;\n        this.model.updateMetaData({ order: nextOrder });\n    }\n\n    toggleStacked() {\n        const { stacked } = this.model.metaData;\n        this.model.updateMetaData({ stacked: !stacked });\n    }\n\n    toggleCumulated() {\n        const { cumulated } = this.model.metaData;\n        this.model.updateMetaData({ cumulated: !cumulated });\n    }\n}\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport class GraphSearchModel extends SearchModel {\n    _getIrFilterDescription() {\n        this.preparingIrFilterDescription = true;\n        const result = super._getIrFilterDescription(...arguments);\n        this.preparingIrFilterDescription = false;\n        return result;\n    }\n\n    _getSearchItemGroupBys(activeItem) {\n        const { searchItemId } = activeItem;\n        const { context, type } = this.searchItems[searchItemId];\n        if (!this.preparingIrFilterDescription && type === \"favorite\" && context.graph_groupbys) {\n            return context.graph_groupbys;\n        }\n        return super._getSearchItemGroupBys(...arguments);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { GraphArchParser } from \"./graph_arch_parser\";\nimport { GraphModel } from \"./graph_model\";\nimport { GraphController } from \"./graph_controller\";\nimport { GraphRenderer } from \"./graph_renderer\";\nimport { GraphSearchModel } from \"./graph_search_model\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const graphView = {\n    type: \"graph\",\n    Controller: GraphController,\n    Renderer: GraphRenderer,\n    Model: GraphModel,\n    ArchParser: GraphArchParser,\n    SearchModel: GraphSearchModel,\n    searchMenuTypes: [\"filter\", \"groupBy\", \"comparison\", \"favorite\"],\n    buttonTemplate: \"web.GraphView.Buttons\",\n\n    props: (genericProps, view) => {\n        let modelParams;\n        if (genericProps.state) {\n            modelParams = genericProps.state.metaData;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n            const parser = new view.ArchParser();\n            const archInfo = parser.parse(arch, fields);\n            modelParams = {\n                disableLinking: Boolean(archInfo.disableLinking),\n                fieldAttrs: archInfo.fieldAttrs,\n                fields: fields,\n                groupBy: archInfo.groupBy,\n                measure: archInfo.measure || \"__count\",\n                viewMeasures: archInfo.measures,\n                mode: archInfo.mode || \"bar\",\n                order: archInfo.order || null,\n                resModel: resModel,\n                stacked: \"stacked\" in archInfo ? archInfo.stacked : true,\n                cumulated: archInfo.cumulated || false,\n                cumulatedStart: archInfo.cumulatedStart || false,\n                title: archInfo.title || _t(\"Untitled\"),\n            };\n        }\n\n        return {\n            ...genericProps,\n            modelParams,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nviewRegistry.add(\"graph\", graphView);\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\n\nexport class PivotArchParser {\n    parse(arch) {\n        const archInfo = {\n            activeMeasures: [], // store the defined active measures\n            colGroupBys: [], // store the defined group_by used on cols\n            defaultOrder: null,\n            fieldAttrs: {},\n            rowGroupBys: [], // store the defined group_by used on rows\n            widgets: {}, // wigdets defined in the arch\n        };\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"pivot\": {\n                    if (node.hasAttribute(\"disable_linking\")) {\n                        archInfo.disableLinking = exprToBoolean(\n                            node.getAttribute(\"disable_linking\")\n                        );\n                    }\n                    if (node.hasAttribute(\"default_order\")) {\n                        archInfo.defaultOrder = node.getAttribute(\"default_order\");\n                    }\n                    if (node.hasAttribute(\"string\")) {\n                        archInfo.title = node.getAttribute(\"string\");\n                    }\n                    if (node.hasAttribute(\"display_quantity\")) {\n                        archInfo.displayQuantity = exprToBoolean(\n                            node.getAttribute(\"display_quantity\")\n                        );\n                    }\n                    break;\n                }\n                case \"field\": {\n                    let fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n\n                    archInfo.fieldAttrs[fieldName] = {};\n                    if (node.hasAttribute(\"string\")) {\n                        archInfo.fieldAttrs[fieldName].string = node.getAttribute(\"string\");\n                    }\n                    if (\n                        node.getAttribute(\"invisible\") === \"True\" ||\n                        node.getAttribute(\"invisible\") === \"1\"\n                    ) {\n                        archInfo.fieldAttrs[fieldName].isInvisible = true;\n                        break;\n                    }\n\n                    if (node.hasAttribute(\"interval\")) {\n                        fieldName += \":\" + node.getAttribute(\"interval\");\n                    }\n                    if (node.hasAttribute(\"widget\")) {\n                        archInfo.widgets[fieldName] = node.getAttribute(\"widget\");\n                    }\n                    if (node.getAttribute(\"type\") === \"measure\" || node.hasAttribute(\"operator\")) {\n                        archInfo.activeMeasures.push(fieldName);\n                    }\n                    if (node.getAttribute(\"type\") === \"col\") {\n                        archInfo.colGroupBys.push(fieldName);\n                    }\n                    if (node.getAttribute(\"type\") === \"row\") {\n                        archInfo.rowGroupBys.push(fieldName);\n                    }\n                    break;\n                }\n            }\n        });\n\n        return archInfo;\n    }\n}\n", "import { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class PivotController extends Component {\n    static template = \"web.PivotView\";\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);\n\n        useSetupAction({\n            rootRef: useRef(\"root\"),\n            getLocalState: () => {\n                const { data, metaData } = this.model;\n                return { data, metaData };\n            },\n            getContext: () => this.getContext(),\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n    /**\n     * @returns {Object}\n     */\n    getContext() {\n        return {\n            pivot_measures: this.model.metaData.activeMeasures,\n            pivot_column_groupby: this.model.metaData.fullColGroupBys,\n            pivot_row_groupby: this.model.metaData.fullRowGroupBys,\n        };\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { CustomGroupByItem } from \"@web/search/custom_group_by_item/custom_group_by_item\";\nimport { PropertiesGroupByItem } from \"@web/search/properties_group_by_item/properties_group_by_item\";\nimport { getIntervalOptions } from \"@web/search/utils/dates\";\nimport { FACET_ICONS, GROUPABLE_TYPES } from \"@web/search/utils/misc\";\n\nexport class PivotHeader extends Component {\n    static template = \"web.PivotHeader\";\n    static components = {\n        CustomGroupByItem,\n        Dropdown,\n        CheckboxItem,\n        PropertiesGroupByItem,\n    };\n    static defaultProps = {\n        isInHead: false,\n        isXAxis: false,\n        showCaretDown: false,\n    };\n    static props = {\n        cell: Object,\n        isInHead: { type: Boolean, optional: true },\n        isXAxis: { type: Boolean, optional: true },\n        customGroupBys: Object,\n        onAddCustomGroupBy: Function,\n        onItemSelected: Function,\n        onClick: Function,\n        slots: { optional: true },\n    };\n\n    setup() {\n        this.icon = FACET_ICONS.groupBy;\n        const fields = [];\n        for (const [fieldName, field] of Object.entries(this.env.searchModel.searchViewFields)) {\n            if (this.validateField(fieldName, field)) {\n                fields.push(Object.assign({ name: fieldName }, field));\n            }\n        }\n        this.fields = sortBy(fields, \"string\");\n        this.l10n = localization;\n        this.dropdownState = useDropdownState();\n\n        useBus(this.env.searchModel, \"update\", this.render);\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get hideCustomGroupBy() {\n        return this.env.searchModel.hideCustomGroupBy || false;\n    }\n\n    /**\n     * @returns {Object[]}\n     */\n    get items() {\n        let items = this.env.searchModel.getSearchItems(\n            (searchItem) =>\n                [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) && !searchItem.custom\n        );\n        if (items.length === 0) {\n            items = this.fields;\n        }\n\n        // Add custom groupbys\n        let groupNumber = 1 + Math.max(0, ...items.map(({ groupNumber: n }) => n));\n        for (const [fieldName, customGroupBy] of this.props.customGroupBys.entries()) {\n            items.push({ ...customGroupBy, name: fieldName, groupNumber: groupNumber++ });\n        }\n\n        return items.map((item) => ({\n            ...item,\n            id: item.id || item.name,\n            fieldName: item.fieldName || item.name,\n            description: item.description || item.string,\n            isActive: false,\n            options:\n                item.options || [\"date\", \"datetime\"].includes(item.type)\n                    ? getIntervalOptions()\n                    : undefined,\n        }));\n    }\n\n    get cell() {\n        return this.props.cell;\n    }\n\n    /**\n     * Retrieve the padding of a left header.\n     * @returns {Number} Padding\n     */\n    get padding() {\n        return 5 + this.cell.indent * 30;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @param {Object} field\n     * @returns {boolean}\n     */\n    validateField(fieldName, field) {\n        const { groupable, type } = field;\n        return (\n            groupable &&\n            fieldName !== \"id\" &&\n            GROUPABLE_TYPES.includes(type)\n        );\n    }\n\n    /**\n     * @override\n     * @param {Object} param0\n     * @param {number} param0.itemId\n     * @param {number} [param0.optionId]\n     */\n    onGroupBySelected({ itemId, optionId }) {\n        // Here, we purposely do not call super.onGroupBySelected as we don't want\n        // to change the group-by on the model, only inside the pivot\n        const item = this.items.find(({ id }) => id === itemId);\n        this.props.onItemSelected({\n            itemId,\n            optionId,\n            fieldName: item.fieldName,\n            interval: optionId,\n            groupId: this.cell.groupId,\n        });\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    onAddCustomGroup(fieldName) {\n        this.props.onAddCustomGroupBy(fieldName);\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onClick(event) {\n        if (this.cell.isLeaf && !this.cell.isFolded) {\n            this.dropdownState.open();\n        }\n        this.props.onClick();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { cartesian, sections, sortBy, symmetricalDifference } from \"@web/core/utils/arrays\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { DEFAULT_INTERVAL } from \"@web/search/utils/dates\";\nimport { addPropertyFieldDefs, Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\n\n/**\n * @param {number} value\n * @param {number} comparisonValue\n * @returns {number}\n */\nfunction computeVariation(value, comparisonValue) {\n    if (isNaN(value) || isNaN(comparisonValue)) {\n        return NaN;\n    }\n    if (comparisonValue === 0) {\n        if (value === 0) {\n            return 0;\n        } else if (value > 0) {\n            return 1;\n        } else {\n            return -1;\n        }\n    }\n    return (value - comparisonValue) / Math.abs(comparisonValue);\n}\n\n/**\n * Pivot Model\n *\n * The pivot model keeps an in-memory representation of the pivot table that is\n * displayed on the screen.  The exact layout of this representation is not so\n * simple, because a pivot table is at its core a 2-dimensional object, but\n * with a 'list' component: some rows/cols can be expanded so we zoom into the\n * structure.\n *\n * However, we need to be able to manipulate the data in a somewhat efficient\n * way, and to transform it into a list of lines to be displayed by the renderer.\n *\n * Basicaly the pivot table presents aggregated values for various groups of records\n * in one domain. If a comparison is asked for, two domains are considered.\n *\n * Let us consider a simple example and let us fix the vocabulary (let us suppose we are in June 2020):\n * ___________________________________________________________________________________________________________________________________________\n * |                    |   Total                                                                                                             |\n * |                    |_____________________________________________________________________________________________________________________|\n * |                    |   Sale Team 1                         |  Sale Team 2                         |                                      |\n * |                    |_______________________________________|______________________________________|______________________________________|\n * |                    |   Sales total                         |  Sales total                         |  Sales total                         |\n * |                    |_______________________________________|______________________________________|______________________________________|\n * |                    |   May 2020   | June 2020  | Variation |  May 2020   | June 2020  | Variation |  May 2020   | June 2020  | Variation |\n * |____________________|______________|____________|___________|_____________|____________|___________|_____________|____________|___________|\n * | Total              |     85       |     110    |  29.4%    |     40      |    30      |   -25%    |    125      |    140     |     12%   |\n * |    Europe          |     25       |     35     |    40%    |     40      |    30      |   -25%    |     65      |     65     |      0%   |\n * |        Brussels    |      0       |     15     |   100%    |     30      |    30      |     0%    |     30      |     45     |     50%   |\n * |        Paris       |     25       |     20     |   -20%    |     10      |     0      |  -100%    |     35      |     20     |  -42.8%   |\n * |    North America   |     60       |     75     |    25%    |             |            |           |     60      |     75     |     25%   |\n * |        Washington  |     60       |     75     |    25%    |             |            |           |     60      |     75     |     25%   |\n * |____________________|______________|____________|___________|_____________|____________|___________|_____________|____________|___________|\n *\n *\n * META DATA:\n *\n * In the above pivot table, the records have been grouped using the fields\n *\n *      continent_id, city_id\n *\n * for rows and\n *\n *      sale_team_id\n *\n * for columns.\n *\n * The measure is the field 'sales_total'.\n *\n * Two domains are considered: 'May 2020' and 'June 2020'.\n *\n * In the model,\n *\n *      - rowGroupBys is the list [continent_id, city_id]\n *      - colGroupBys is the list [sale_team_id]\n *      - measures is the list [sales_total]\n *      - domains is the list [d1, d2] with d1 and d2 domain expressions\n *          for say sale_date in May 2020 and June 2020, for instance\n *          d1 = [['sale_date', >=, 2020-05-01], ['sale_date', '<=', 2020-05-31]]\n *      - origins is the list ['May 2020', 'June 2020']\n *\n * DATA:\n *\n * Recall that a group is constituted by records (in a given domain)\n * that have the same (raw) values for a list of fields.\n * Thus the group itself is identified by this list and the domain.\n * In comparison mode, the same group (forgetting the domain part or 'originIndex')\n * can be eventually found in the two domains.\n * This defines the way in which the groups are identified or not.\n *\n * In the above table, (forgetting the domain) the following groups are found:\n *\n *      the 'row groups'\n *      - Total\n *      - Europe\n *      - America\n *      - Europe, Brussels\n *      - Europe, Paris\n *      - America, Washington\n *\n *      the 'col groups'\n *\n *      - Total\n *      - Sale Team 1\n *      - Sale Team 2\n *\n *      and all non trivial combinations of row groups and col groups\n *\n *      - Europe, Sale Team 1\n *      - Europe, Brussels, Sale Team 2\n *      - America, Washington, Sale Team 1\n *      - ...\n *\n * The list of fields is created from the concatenation of two lists of fields, the first in\n *\n * [], [f1], [f1, f2], ... [f1, f2, ..., fn]  for [f1, f2, ..., fn] the full list of groupbys\n * (called rowGroupBys) used to create row groups\n *\n * In the example: [], [continent_id], [continent_id, city_id].\n *\n * and the second in\n * [], [g1], [g1, g2], ... [g1, g2, ..., gm]  for [g1, g2, ..., gm] the full list of groupbys\n * (called colGroupBys) used to create col groups.\n *\n * In the example: [], [sale_team_id].\n *\n * Thus there are (n+1)*(m+1) lists of fields possible.\n *\n * In the example: 6 lists possible, namely [],\n *                                          [continent_id], [sale_team_id],\n *                                          [continent_id, sale_team_id], [continent_id, city_id],\n *                                          [continent_id, city_id, sale_team_id]\n *\n * A given list is thus of the form [f1,..., fi, g1,..., gj] or better [[f1,...,fi], [g1,...,gj]]\n *\n * For each list of fields possible and each domain considered, one read_group is done\n * and gives results of the form (an exception for list [])\n *\n * g = {\n *  f1: v1, ..., fi: vi,\n *  g1: w1, ..., gj: wj,\n *  m1: x1, ..., mk: xk,\n *  __count: c,\n *  __domain: d\n * }\n *\n * where v1,...,vi,w1,...,Wj are 'values' for the corresponding fields and\n * m1,...,mk are the fields selected as measures.\n *\n * For example, g = {\n *      continent_id: [1, 'Europe']\n *      sale_team_id: [1, 'Sale Team 1']\n *      sales_count: 25,\n *      __count: 4\n *      __domain: [\n *                  ['sale_date', >=, 2020-05-01], ['sale_date', '<=', 2020-05-31],\n *                  ['continent_id', '=', 1],\n *                  ['sale_team_id', '=', 1]\n *                ]\n * }\n *\n * Thus the above group g is fully determined by [[v1,...,vi], [w1,...,wj]] and the base domain\n * or the corresponding 'originIndex'.\n *\n * When j=0, g corresponds to a row group (or also row header) and is of the form [[v1,...,vi], []] or more simply [v1,...vi]\n * (not forgetting the list [v1,...vi] comes from left).\n * When i=0, g corresponds to a col group (or col header) and is of the form [[], [w1,...,wj]] or more simply [w1,...,wj].\n *\n * A generic group g as above [[v1,...,vi], [w1,...,wj]] corresponds to the two headers [[v1,...,vi], []]\n * and [[], [w1,...,wj]].\n *\n * Here is a description of the data structure manipulated by the pivot model.\n *\n * Five objects contain all the data from the read_groups\n *\n *      - rowGroupTree: contains information on row headers\n *             the nodes correspond to the groups of the form [[v1,...,vi], []]\n *             The root is [[], []].\n *             A node [[v1,...,vl], []] has as direct children the nodes of the form [[v1,...,vl,v], []],\n *             this means that a direct child is obtained by grouping records using the single field fi+1\n *\n *             The structure at each level is of the form\n *\n *             {\n *                  root: {\n *                      values: [v1,...,vl],\n *                      labels: [la1,...,lal]\n *                  },\n *                  directSubTrees: {\n *                      v => {\n *                              root: {\n *                                  values: [v1,...,vl,v]\n *                                  labels: [label1,...,labell,label]\n *                              },\n *                              directSubTrees: {...}\n *                          },\n *                      v' => {...},\n *                      ...\n *                  }\n *             }\n *\n *             (directSubTrees is a Map instance)\n *\n *             In the example, the rowGroupTree is:\n *\n *             {\n *                  root: {\n *                      values: [],\n *                      labels: []\n *                  },\n *                  directSubTrees: {\n *                      1 => {\n *                              root: {\n *                                  values: [1],\n *                                  labels: ['Europe'],\n *                              },\n *                              directSubTrees: {\n *                                  1 => {\n *                                          root: {\n *                                              values: [1, 1],\n *                                              labels: ['Europe', 'Brussels'],\n *                                          },\n *                                          directSubTrees: new Map(),\n *                                  },\n *                                  2 => {\n *                                          root: {\n *                                              values: [1, 2],\n *                                              labels: ['Europe', 'Paris'],\n *                                          },\n *                                          directSubTrees: new Map(),\n *                                  },\n *                              },\n *                          },\n *                      2 => {\n *                              root: {\n *                                  values: [2],\n *                                  labels: ['America'],\n *                              },\n *                              directSubTrees: {\n *                                  3 => {\n *                                          root: {\n *                                              values: [2, 3],\n *                                              labels: ['America', 'Washington'],\n *                                          }\n *                                          directSubTrees: new Map(),\n *                                  },\n *                              },\n *                      },\n *                  },\n *             }\n *\n *      - colGroupTree: contains information on col headers\n *              The same as above with right instead of left\n *\n *      - measurements: contains information on measure values for all the groups\n *\n *              the object keys are of the form JSON.stringify([[v1,...,vi], [w1,...,wj]])\n *              and values are arrays of length equal to number of origins containing objects of the form\n *                  {m1: x1,...,mk: xk}\n *              The structure looks like\n *\n *              {\n *                  JSON.stringify([[], []]): [{m1: x1,...,mk: xk}, {m1: x1',...,mk: xk'},...]\n *                  ....\n *                  JSON.stringify([[v1,...,vi], [w1,...,wj]]): [{m1: y1',...,mk: yk'}, {m1: y1',...,mk: yk'},...],\n *                  ....\n *                  JSON.stringify([[v1,...,vn], [w1,...,wm]]): [{m1: z1',...,mk: zk'}, {m1: z1',...,mk: zk'},...],\n *              }\n *              Thus the structure contains all information for all groups and all origins on measure values.\n *\n *\n *              this.measurments[\"[[], []]\"][0]['foo'] gives the value of the measure 'foo' for the group 'Total' and the\n *              first domain (origin).\n *\n *              In the example:\n *                  {\n *                      \"[[], []]\": [{'sales_total': 125}, {'sales_total': 140}]                      (total/total)\n *                      ...\n *                      \"[[1, 2], [2]]\": [{'sales_total': 10}, {'sales_total': 0}]                   (Europe/Paris/Sale Team 2)\n *                      ...\n *                  }\n *\n *      - counts: contains information on the number of records in each groups\n *              The structure is similar to the above but the arrays contains numbers (counts)\n *      - groupDomains:\n *              The structure is similar to the above but the arrays contains domains\n *\n *      With this light data structures, all manipulation done by the model are eased and redundancies are limited.\n *      Each time a rendering or an export of the data has to be done, the pivot table is generated by the getTable function.\n */\n\n/**\n * @typedef Meta\n * @property {string[]} activeMeasures\n * @property {string[]} colGroupBys\n * @property {boolean} disableLinking\n * @property {Object} fields\n * @property {Object} measures\n * @property {string} resModel\n * @property {string[]} rowGroupBys\n * @property {string} title\n * @property {boolean} useSampleModel\n * @property {Object} widgets\n * @property {Map} customGroupBys\n * @property {string[]} expandedRowGroupBys\n * @property {string[]} expandedColGroupBys\n * @property {Object} sortedColumn\n * @property {Array[]} domains\n * @property {string[]} origins\n */\n\n/**\n * @typedef Data\n * @property {Object} colGroupTree\n * @property {Object} rowGroupTree\n * @property {Object} groupDomains\n * @property {Object} measurements\n * @property {Object} counts\n * @property {Object} numbering\n */\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\n/**\n * @typedef Config\n * @property {MetaData} metaData\n * @property {Data} data\n */\n\nexport class PivotModel extends Model {\n    /**\n     * @override\n     * @param {Object} params.metaData\n     * @param {string[]} params.metaData.activeMeasures\n     * @param {string[]} params.metaData.colGroupBys\n     * @param {Object} params.metaData.fields\n     * @param {Object[]} params.metaData.measures\n     * @param {string} params.metaData.resModel\n     * @param {string[]} params.metaData.rowGroupBys\n     * @param {string|null} params.metaData.defaultOrder\n     * @param {boolean} params.metaData.disableLinking\n     * @param {boolean} params.metaData.useSampleModel\n     * @param {Map} [params.metaData.customGroupBys={}]\n     * @param {string[]} [params.metaData.expandedColGroupBys=[]]\n     * @param {string[]} [params.metaData.expandedRowGroupBys=[]]\n     * @param {Object|null} [params.metaData.sortedColumn=null]\n     * @param {Object} [params.data] previously exported data\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _loadData = this._loadData.bind(this);\n        this._loadData = (...args) => {\n            return this.race.add(_loadData(...args));\n        };\n\n        let sortedColumn = params.metaData.sortedColumn || null;\n        if (!sortedColumn && params.metaData.defaultOrder) {\n            const defaultOrder = params.metaData.defaultOrder.split(\" \");\n            sortedColumn = {\n                groupId: [[], []],\n                measure: defaultOrder[0],\n                order: defaultOrder[1] ? defaultOrder[1] : \"asc\",\n            };\n        }\n\n        this.searchParams = {\n            context: {},\n            domain: [],\n            domains: [],\n            groupBy: [],\n        };\n        this.data = params.data || {\n            colGroupTree: null,\n            rowGroupTree: null,\n            groupDomains: {},\n            measurements: {},\n            counts: {},\n            numbering: {},\n        };\n        const metaData = Object.assign({}, params.metaData, {\n            customGroupBys: params.metaData.customGroupBys || new Map(),\n            expandedRowGroupBys: params.metaData.expandedRowGroupBys || [],\n            expandedColGroupBys: params.metaData.expandedColGroupBys || [],\n            sortedColumn,\n        });\n        this.metaData = this._buildMetaData(metaData);\n\n        this.reload = false; // used to discriminate between the first load and subsequent reloads\n        this.nextActiveMeasures = null; // allows to toggle several measures consecutively\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add a groupBy to rowGroupBys or colGroupBys according to provided type.\n     *\n     * @param {Object} params\n     * @param {Array[]} params.groupId\n     * @param {string} params.fieldName\n     * @param {'row'|'col'} params.type\n     * @param {boolean} [params.custom=false]\n     * @param {string} [params.interval]\n     */\n    async addGroupBy(params) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const { groupId, fieldName, type, custom } = params;\n        let { interval } = params;\n        const metaData = this._buildMetaData();\n        if (custom && !metaData.customGroupBys.has(fieldName)) {\n            const field = metaData.fields[fieldName];\n            if (!interval && [\"date\", \"datetime\"].includes(field.type)) {\n                interval = DEFAULT_INTERVAL;\n            }\n            metaData.customGroupBys.set(fieldName, {\n                ...field,\n                id: fieldName,\n            });\n        }\n\n        let groupBy = fieldName;\n        if (interval) {\n            groupBy = `${groupBy}:${interval}`;\n        }\n        if (type === \"row\") {\n            metaData.expandedRowGroupBys.push(groupBy);\n        } else {\n            metaData.expandedColGroupBys.push(groupBy);\n        }\n        const config = { metaData, data: this.data };\n        await this._expandGroup(groupId, type, config);\n        this.metaData = metaData;\n        this.notify();\n    }\n    /**\n     * Close the group with id given by groupId. A type must be specified\n     * in case groupId is [[], []] (the id of the group 'Total') because this\n     * group is present in both colGroupTree and rowGroupTree.\n     *\n     * @param {Array[]} groupId\n     * @param {'row'|'col'} type\n     */\n    closeGroup(groupId, type) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloading the table\n        }\n\n        let groupBys;\n        let expandedGroupBys;\n        let keyPart;\n        let group;\n        let tree;\n        if (type === \"row\") {\n            groupBys = this.metaData.rowGroupBys;\n            expandedGroupBys = this.metaData.expandedRowGroupBys;\n            tree = this.data.rowGroupTree;\n            group = this._findGroup(this.data.rowGroupTree, groupId[0]);\n            keyPart = 0;\n        } else {\n            groupBys = this.metaData.colGroupBys;\n            expandedGroupBys = this.metaData.expandedColGroupBys;\n            tree = this.data.colGroupTree;\n            group = this._findGroup(this.data.colGroupTree, groupId[1]);\n            keyPart = 1;\n        }\n\n        const groupIdPart = groupId[keyPart];\n        const range = groupIdPart.map((_, index) => index);\n        function keep(key) {\n            const idPart = JSON.parse(key)[keyPart];\n            return (\n                range.some((index) => groupIdPart[index] !== idPart[index]) ||\n                idPart.length === groupIdPart.length\n            );\n        }\n        function omitKeys(object) {\n            const newObject = {};\n            for (const key in object) {\n                if (keep(key)) {\n                    newObject[key] = object[key];\n                }\n            }\n            return newObject;\n        }\n        this.data.measurements = omitKeys(this.data.measurements);\n        this.data.counts = omitKeys(this.data.counts);\n        this.data.groupDomains = omitKeys(this.data.groupDomains);\n\n        group.directSubTrees.clear();\n        delete group.sortedKeys;\n        var newGroupBysLength = this._getTreeHeight(tree) - 1;\n        if (newGroupBysLength <= groupBys.length) {\n            expandedGroupBys.splice(0);\n            groupBys.splice(newGroupBysLength);\n        } else {\n            expandedGroupBys.splice(newGroupBysLength - groupBys.length);\n        }\n        this.notify();\n    }\n    /**\n     * Reload the view with the current rowGroupBys and colGroupBys\n     * This is the easiest way to expand all the groups that are not expanded\n     */\n    async expandAll() {\n        const config = { metaData: this.metaData, data: this.data };\n        await this._loadData(config, false);\n        this.notify();\n    }\n    /**\n     * Expand a group by using groupBy to split it and trigger a re-rendering.\n     *\n     * @param {Object} group\n     * @param {'row'|'col'} type\n     */\n    async expandGroup(groupId, type) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const config = { metaData: this.metaData, data: this.data };\n        await this._expandGroup(groupId, type, config);\n        this.notify();\n    }\n    /**\n     * Export model data in a form suitable for an easy encoding of the pivot\n     * table in excell.\n     *\n     * @returns {Object}\n     */\n    exportData() {\n        const measureCount = this.metaData.activeMeasures.length;\n        const originCount = this.metaData.origins.length;\n\n        const table = this.getTable();\n\n        // process headers\n        const headers = table.headers;\n        let colGroupHeaderRows;\n        let measureRow = [];\n        let originRow = [];\n\n        function processHeader(header) {\n            const inTotalColumn = header.groupId[1].length === 0;\n            return {\n                title: header.title,\n                width: header.width,\n                height: header.height,\n                is_bold: !!header.measure && inTotalColumn,\n            };\n        }\n\n        if (originCount > 1) {\n            colGroupHeaderRows = headers.slice(0, headers.length - 2);\n            measureRow = headers[headers.length - 2].map(processHeader);\n            originRow = headers[headers.length - 1].map(processHeader);\n        } else {\n            colGroupHeaderRows = headers.slice(0, headers.length - 1);\n            measureRow = headers[headers.length - 1].map(processHeader);\n        }\n\n        // remove the empty headers on left side\n        colGroupHeaderRows[0].splice(0, 1);\n\n        colGroupHeaderRows = colGroupHeaderRows.map((headerRow) => {\n            return headerRow.map(processHeader);\n        });\n\n        // process rows\n        const tableRows = table.rows.map((row) => {\n            return {\n                title: row.title,\n                indent: row.indent,\n                values: row.subGroupMeasurements.map((measurement) => {\n                    let value = measurement.value;\n                    if (value === undefined) {\n                        value = \"\";\n                    } else if (measurement.originIndexes.length > 1) {\n                        // in that case the value is a variation and a\n                        // number between 0 and 1\n                        value = value * 100;\n                    }\n                    return {\n                        is_bold: measurement.isBold,\n                        value: value,\n                    };\n                }),\n            };\n        });\n\n        return {\n            model: this.metaData.resModel,\n            title: this.metaData.title,\n            col_group_headers: colGroupHeaderRows,\n            measure_headers: measureRow,\n            origin_headers: originRow,\n            rows: tableRows,\n            measure_count: measureCount,\n            origin_count: originCount,\n        };\n    }\n    /**\n     * Swap the pivot columns and the rows. The flip operation is synchronous.\n     * However, we must wait for a potential pending reload to complete before\n     * flipping the axes. This method is thus async.\n     */\n    async flip() {\n        await this.race.getCurrentProm();\n\n        // swap the data: the main column and the main row\n        let temp = this.data.rowGroupTree;\n        this.data.rowGroupTree = this.data.colGroupTree;\n        this.data.colGroupTree = temp;\n\n        // we need to update the record metaData: (expanded) row and col groupBys\n        temp = this.metaData.rowGroupBys;\n        this.metaData.rowGroupBys = this.metaData.colGroupBys;\n        this.metaData.colGroupBys = temp;\n        temp = this.metaData.expandedColGroupBys;\n        this.metaData.expandedColGroupBys = this.metaData.expandedRowGroupBys;\n        this.metaData.expandedRowGroupBys = temp;\n\n        function twistKey(key) {\n            return JSON.stringify(JSON.parse(key).reverse());\n        }\n\n        function twist(object) {\n            const newObject = {};\n            Object.keys(object).forEach((key) => {\n                const value = object[key];\n                newObject[twistKey(key)] = value;\n            });\n            return newObject;\n        }\n\n        this.data.measurements = twist(this.data.measurements);\n        this.data.counts = twist(this.data.counts);\n        this.data.groupDomains = twist(this.data.groupDomains);\n\n        this.notify();\n    }\n    /**\n     * Returns a domain representation of a group\n     *\n     * @param {Object} group\n     * @param {Array} group.colValues\n     * @param {Array} group.rowValues\n     * @param {number} group.originIndex\n     * @returns {Array[]}\n     */\n    getGroupDomain(group) {\n        const config = { metaData: this.metaData, data: this.data };\n        return this._getGroupDomain(group, config);\n    }\n    /**\n     * Returns a description of the pivot table.\n     *\n     * @returns {Object}\n     */\n    getTable() {\n        const headers = this._getTableHeaders();\n        return {\n            headers: headers,\n            rows: this._getTableRows(this.data.rowGroupTree, headers[headers.length - 1]),\n        };\n    }\n    /**\n     * Returns the total number of columns of the pivot table.\n     *\n     * @returns {integer}\n     */\n    getTableWidth() {\n        var leafCounts = this._getLeafCounts(this.data.colGroupTree);\n        return leafCounts[JSON.stringify(this.data.colGroupTree.root.values)] + 2;\n    }\n    /**\n     * @returns {boolean} true iff there's no data in the table\n     */\n    hasData() {\n        return this._hasData(this.data);\n    }\n    /**\n     * @override\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n        const processedMeasures = processMeasure(searchParams.context.pivot_measures);\n        const activeMeasures = processedMeasures || this.metaData.activeMeasures;\n        const metaData = this._buildMetaData({ activeMeasures });\n        if (!this.reload) {\n            metaData.rowGroupBys =\n                searchParams.context.pivot_row_groupby ||\n                (searchParams.groupBy.length ? searchParams.groupBy : metaData.rowGroupBys);\n            this.reload = true;\n        } else {\n            metaData.rowGroupBys = searchParams.groupBy.length\n                ? searchParams.groupBy\n                : searchParams.context.pivot_row_groupby || metaData.rowGroupBys;\n        }\n        metaData.colGroupBys =\n            searchParams.context.pivot_column_groupby || this.metaData.colGroupBys;\n\n        if (JSON.stringify(metaData.rowGroupBys) !== JSON.stringify(this.metaData.rowGroupBys)) {\n            metaData.expandedRowGroupBys = [];\n        }\n        if (JSON.stringify(metaData.colGroupBys) !== JSON.stringify(this.metaData.colGroupBys)) {\n            metaData.expandedColGroupBys = [];\n        }\n\n        const allActivesMeasures = new Set(this.metaData.activeMeasures);\n        if (processedMeasures) {\n            processedMeasures.forEach((e) => allActivesMeasures.add(e));\n        }\n\n        metaData.measures = computeReportMeasures(metaData.fields, metaData.fieldAttrs, [\n            ...allActivesMeasures,\n        ]);\n        const config = { metaData, data: this.data };\n        await addPropertyFieldDefs(\n            this.orm,\n            metaData.resModel,\n            searchParams.context,\n            metaData.fields,\n            new Set([...metaData.rowGroupBys, ...metaData.colGroupBys])\n        );\n        return this._loadData(config);\n    }\n    /**\n     * Sort the rows, depending on the values of a given column.  This is an\n     * in-memory sort.\n     *\n     * @param {Object} sortedColumn\n     * @param {number[]} sortedColumn.groupId\n     */\n    sortRows(sortedColumn) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const config = { metaData: this.metaData, data: this.data };\n        this._sortRows(sortedColumn, config);\n\n        this.notify();\n    }\n    /**\n     * Toggle the active state for a given measure, then reload the data\n     * if this turns out to be necessary.\n     *\n     * @param {string} fieldName\n     * @returns {Promise}\n     */\n    async toggleMeasure(fieldName) {\n        const metaData = this._buildMetaData();\n        this.nextActiveMeasures = this.nextActiveMeasures || metaData.activeMeasures;\n        metaData.activeMeasures = this.nextActiveMeasures;\n        const index = metaData.activeMeasures.indexOf(fieldName);\n        if (index !== -1) {\n            // in this case, we already have all data in memory, no need to\n            // actually reload a lesser amount of information (but still, we need\n            // to wait in case there is a pending load)\n            metaData.activeMeasures.splice(index, 1);\n            await Promise.resolve(this.race.getCurrentProm());\n            this.metaData = metaData;\n        } else {\n            metaData.activeMeasures.push(fieldName);\n            const config = { metaData, data: this.data };\n            await this._loadData(config);\n            this.useSampleModel = false;\n        }\n        this.nextActiveMeasures = null;\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add labels/values in the provided groupTree. A new leaf is created in\n     * the groupTree with a root object corresponding to the group with given\n     * labels/values.\n     *\n     * @protected\n     * @param {Object} groupTree, either this.data.rowGroupTree or this.data.colGroupTree\n     * @param {string[]} labels\n     * @param {Array} values\n     */\n    _addGroup(groupTree, labels, values) {\n        let tree = groupTree;\n        // we assume here that the group with value value.slice(value.length - 2) has already been added.\n        values.slice(0, values.length - 1).forEach(function (value) {\n            tree = tree.directSubTrees.get(value);\n        });\n        const value = values[values.length - 1];\n        if (tree.directSubTrees.has(value)) {\n            return;\n        }\n        tree.directSubTrees.set(value, {\n            root: {\n                labels: labels,\n                values: values,\n            },\n            directSubTrees: new Map(),\n        });\n    }\n    /**\n     * Return a copy of this.metaData, extended with optional params. This is useful\n     * for async methods that need to modify this.metaData, but it can't be done in\n     * place directly for the model to be concurrency proof (so they work on a\n     * copy and commit it at the end).\n     *\n     * @protected\n     * @param {Object} params\n     * @returns {Object}\n     */\n    _buildMetaData(params) {\n        const metaData = Object.assign({}, this.metaData, params);\n        metaData.activeMeasures = [...metaData.activeMeasures];\n        metaData.colGroupBys = [...metaData.colGroupBys];\n        metaData.rowGroupBys = [...metaData.rowGroupBys];\n        metaData.expandedColGroupBys = [...metaData.expandedColGroupBys];\n        metaData.expandedRowGroupBys = [...metaData.expandedRowGroupBys];\n        metaData.customGroupBys = new Map([...metaData.customGroupBys]);\n        // shallow copy sortedColumn because we never modify groupId in place\n        metaData.sortedColumn = metaData.sortedColumn ? { ...metaData.sortedColumn } : null;\n        if (this.searchParams.comparison) {\n            const domains = this.searchParams.comparison.domains.slice().reverse();\n            metaData.domains = domains.map((d) => d.arrayRepr);\n            metaData.origins = domains.map((d) => d.description);\n        } else {\n            metaData.domains = [this.searchParams.domain];\n            metaData.origins = [\"\"];\n        }\n        Object.defineProperty(metaData, \"fullColGroupBys\", {\n            get() {\n                return metaData.colGroupBys.concat(metaData.expandedColGroupBys);\n            },\n        });\n        Object.defineProperty(metaData, \"fullRowGroupBys\", {\n            get() {\n                return metaData.rowGroupBys.concat(metaData.expandedRowGroupBys);\n            },\n        });\n        return metaData;\n    }\n    /**\n     * Expand a group by using groupBy to split it.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {'row'|'col'} type\n     * @param {Config} config\n     */\n    async _expandGroup(groupId, type, config) {\n        const { metaData } = config;\n        const group = {\n            rowValues: groupId[0],\n            colValues: groupId[1],\n            type: type,\n        };\n        const groupValues = type === \"row\" ? groupId[0] : groupId[1];\n        const groupBys = type === \"row\" ? metaData.fullRowGroupBys : metaData.fullColGroupBys;\n        if (groupValues.length >= groupBys.length) {\n            throw new Error(\"Cannot expand group\");\n        }\n        const groupBy = groupBys[groupValues.length];\n        let leftDivisors;\n        let rightDivisors;\n        if (group.type === \"row\") {\n            leftDivisors = [[groupBy]];\n            rightDivisors = sections(metaData.fullColGroupBys);\n        } else {\n            leftDivisors = sections(metaData.fullRowGroupBys);\n            rightDivisors = [[groupBy]];\n        }\n        const divisors = cartesian(leftDivisors, rightDivisors);\n        delete group.type;\n        await this._subdivideGroup(group, divisors, config);\n    }\n    /**\n     * Find a group with given values in the provided groupTree, either\n     * this.rowGrouptree or this.data.colGroupTree.\n     *\n     * @protected\n     * @param {Object} groupTree\n     * @param {Array} values\n     * @returns {Object}\n     */\n    _findGroup(groupTree, values) {\n        let tree = groupTree;\n        values.slice(0, values.length).forEach((value) => {\n            tree = tree.directSubTrees.get(value);\n        });\n        return tree;\n    }\n    /**\n     * In case originIndex is an array of length 1, thus a single origin\n     * index, returns the given measure for a group determined by the id\n     * groupId and the origin index.\n     * If originIndexes is an array of length 2, we compute the variation\n     * of the measure values for the groups determined by groupId and the\n     * different origin indexes.\n     *\n     * @protected\n     * @param {Array[]} groupId\n     * @param {string} measure\n     * @param {number[]} originIndexes\n     * @param {Config} config\n     * @returns {number}\n     */\n    _getCellValue(groupId, measure, originIndexes, config) {\n        var key = JSON.stringify(groupId);\n        if (!config.data.measurements[key]) {\n            return;\n        }\n        var values = originIndexes.map((originIndex) => {\n            return config.data.measurements[key][originIndex][measure];\n        });\n        if (originIndexes.length > 1) {\n            return computeVariation(values[1], values[0]);\n        } else {\n            return values[0];\n        }\n    }\n    /**\n     * @protected\n     * @param {string[]} rowGroupBy\n     * @param {string[]} colGroupBy\n     * @returns {string[]}\n     */\n    _getGroupBySpecs(rowGroupBy, colGroupBy) {\n        const set = rowGroupBy.concat(colGroupBy).reduce((acc, gb) => {\n            acc.add(this._normalize(gb));\n            return acc;\n        }, new Set());\n        return [...set];\n    }\n    /**\n     * Returns a domain representation of a group\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Array} group.colValues\n     * @param {Array} group.rowValues\n     * @param {number} group.originIndex\n     * @param {Config} config\n     * @returns {Array[]}\n     */\n    _getGroupDomain(group, config) {\n        const { data } = config;\n        var key = JSON.stringify([group.rowValues, group.colValues]);\n        return data.groupDomains[key][group.originIndex];\n    }\n    /**\n     * Returns the group sanitized labels.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} groupBys\n     * @param {Config} config\n     * @returns {string[]}\n     */\n    _getGroupLabels(group, groupBys, config) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            return this._sanitizeLabel(group[groupBy], groupBy, config);\n        });\n    }\n    /**\n     * Returns a promise that returns the annotated read_group results\n     * corresponding to a partition of the given group obtained using the given\n     * rowGroupBy and colGroupBy.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} rowGroupBy\n     * @param {string[]} colGroupBy\n     * @param {Object} params\n     */\n    async _getGroupSubdivision(group, rowGroupBy, colGroupBy, params) {\n        const groupBy = this._getGroupBySpecs(rowGroupBy, colGroupBy);\n        const subGroups = await this._getSubGroups(groupBy, params);\n        return {\n            group,\n            subGroups,\n            rowGroupBy: rowGroupBy,\n            colGroupBy: colGroupBy,\n        };\n    }\n\n    /**\n     * Returns the group sanitized values.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} groupBys\n     * @returns {Array}\n     */\n    _getGroupValues(group, groupBys) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            return this._sanitizeValue(group[groupBy]);\n        });\n    }\n    /**\n     * Returns the leaf counts of each group inside the given tree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @returns {Object} keys are group ids\n     */\n    _getLeafCounts(tree) {\n        const leafCounts = {};\n        let leafCount;\n        if (!tree.directSubTrees.size) {\n            leafCount = 1;\n        } else {\n            leafCount = [...tree.directSubTrees.values()].reduce((acc, subTree) => {\n                const subLeafCounts = this._getLeafCounts(subTree);\n                Object.assign(leafCounts, subLeafCounts);\n                return acc + leafCounts[JSON.stringify(subTree.root.values)];\n            }, 0);\n        }\n\n        leafCounts[JSON.stringify(tree.root.values)] = leafCount;\n        return leafCounts;\n    }\n    /**\n     * Returns the group sanitized measure values for the measures in\n     * this.metaData.activeMeasures (that migth contain '__count', not really a fieldName).\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Config} config\n     * @returns {Array}\n     */\n    _getMeasurements(group, config) {\n        const { metaData } = config;\n        return metaData.activeMeasures.reduce((measurements, measureName) => {\n            var measurement = group[measureName];\n            if (measurement instanceof Array) {\n                // case field is many2one and used as measure and groupBy simultaneously\n                measurement = 1;\n            }\n            if (\n                metaData.measures[measureName].type === \"boolean\" &&\n                measurement instanceof Boolean\n            ) {\n                measurement = measurement ? 1 : 0;\n            }\n            if (metaData.origins.length > 1 && !measurement) {\n                measurement = 0;\n            }\n            measurements[measureName] = measurement;\n            return measurements;\n        }, {});\n    }\n    /**\n     * Returns a description of the measures row of the pivot table\n     *\n     * @protected\n     * @param {Object[]} columns for which measure cells must be generated\n     * @returns {Object[]}\n     */\n    _getMeasuresRow(columns) {\n        const sortedColumn = this.metaData.sortedColumn || {};\n        const measureRow = [];\n\n        columns.forEach((column) => {\n            this.metaData.activeMeasures.forEach((measureName) => {\n                const measureCell = {\n                    groupId: column.groupId,\n                    height: 1,\n                    measure: measureName,\n                    title: this.metaData.measures[measureName].string,\n                    width: 2 * this.metaData.origins.length - 1,\n                };\n                if (\n                    sortedColumn.measure === measureName &&\n                    JSON.stringify(sortedColumn.groupId) === JSON.stringify(column.groupId) // FIXME\n                ) {\n                    measureCell.order = sortedColumn.order;\n                }\n                measureRow.push(measureCell);\n            });\n        });\n\n        return measureRow;\n    }\n    /**\n     * Returns the list of measure specs associated with metaData.activeMeasures, i.e.\n     * a measure 'fieldName' becomes 'fieldName:aggregator' where\n     * aggregator is the value specified on the field 'fieldName' for\n     * the key aggregator.\n     *\n     * @protected\n     * @param {Config} config\n     * @return {string[]}\n     */\n    _getMeasureSpecs(config) {\n        const { metaData } = config;\n        return metaData.activeMeasures.reduce((acc, measure) => {\n            if (measure === \"__count\") {\n                acc.push(measure);\n                return acc;\n            }\n            const field = this.metaData.fields[measure];\n            if (field.type === \"many2one\") {\n                field.aggregator = \"count_distinct\";\n            }\n            if (field.aggregator === undefined) {\n                throw new Error(\n                    \"No aggregate function has been provided for the measure '\" + measure + \"'\"\n                );\n            }\n            acc.push(measure + \":\" + field.aggregator);\n            return acc;\n        }, []);\n    }\n    /**\n     * Make sure that the labels of different many2one values are distinguished\n     * by numbering them if necessary.\n     *\n     * @protected\n     * @param {Array} label\n     * @param {string} fieldName\n     * @param {Config} config\n     * @returns {string}\n     */\n    _getNumberedLabel(label, fieldName, config) {\n        const { data } = config;\n        const id = label[0];\n        const name = label[1];\n        data.numbering[fieldName] = data.numbering[fieldName] || {};\n        data.numbering[fieldName][name] = data.numbering[fieldName][name] || {};\n        const numbers = data.numbering[fieldName][name];\n        numbers[id] = numbers[id] || Object.keys(numbers).length + 1;\n        return name + (numbers[id] > 1 ? \"  (\" + numbers[id] + \")\" : \"\");\n    }\n    /**\n     * Returns a description of the origins row of the pivot table\n     *\n     * @protected\n     * @param {Object[]} columns for which origin cells must be generated\n     * @returns {Object[]}\n     */\n    _getOriginsRow(columns) {\n        const sortedColumn = this.metaData.sortedColumn || {};\n        const originRow = [];\n\n        columns.forEach((column) => {\n            const groupId = column.groupId;\n            const measure = column.measure;\n            const isSorted =\n                sortedColumn.measure === measure &&\n                JSON.stringify(sortedColumn.groupId) === JSON.stringify(groupId); // FIXME\n            const isSortedByOrigin = isSorted && !sortedColumn.originIndexes[1];\n            const isSortedByVariation = isSorted && sortedColumn.originIndexes[1];\n\n            this.metaData.origins.forEach((origin, originIndex) => {\n                const originCell = {\n                    groupId: groupId,\n                    height: 1,\n                    measure: measure,\n                    originIndexes: [originIndex],\n                    title: origin,\n                    width: 1,\n                };\n                if (isSortedByOrigin && sortedColumn.originIndexes[0] === originIndex) {\n                    originCell.order = sortedColumn.order;\n                }\n                originRow.push(originCell);\n\n                if (originIndex > 0) {\n                    const variationCell = {\n                        groupId: groupId,\n                        height: 1,\n                        measure: measure,\n                        originIndexes: [originIndex - 1, originIndex],\n                        title: _t(\"Variation\"),\n                        width: 1,\n                    };\n                    if (isSortedByVariation && sortedColumn.originIndexes[1] === originIndex) {\n                        variationCell.order = sortedColumn.order;\n                    }\n                    originRow.push(variationCell);\n                }\n            });\n        });\n\n        return originRow;\n    }\n    /**\n     * @protected\n     * @param {string[]} groupBy\n     * @param {Object} params\n     * @returns {Promise<Object[]>}\n     */\n    async _getSubGroups(groupBy, params) {\n        const { resModel, groupDomain, measureSpecs, kwargs, mapping } = params;\n        const key = JSON.stringify(groupBy);\n        if (!mapping[key]) {\n            mapping[key] = this.orm.readGroup(resModel, groupDomain, measureSpecs, groupBy, kwargs);\n        }\n        return mapping[key];\n    }\n    /**\n     * Returns the list of header rows of the pivot table: the col group rows\n     * (depending on the col groupbys), the measures row and optionnaly the\n     * origins row (if there are more than one origins).\n     *\n     * @protected\n     * @returns {Object[]}\n     */\n    _getTableHeaders() {\n        const colGroupBys = this.metaData.fullColGroupBys;\n        const height = colGroupBys.length + 1;\n        const measureCount = this.metaData.activeMeasures.length;\n        const originCount = this.metaData.origins.length;\n        const leafCounts = this._getLeafCounts(this.data.colGroupTree);\n        let headers = [];\n        const measureColumns = []; // used to generate the measure cells\n\n        // 1) generate col group rows (total row + one row for each col groupby)\n        const colGroupRows = new Array(height).fill(0).map(() => []);\n        // blank top left cell\n        colGroupRows[0].push({\n            height: height + 1 + (originCount > 1 ? 1 : 0), // + measures rows [+ origins row]\n            title: \"\",\n            width: 1,\n        });\n\n        // col groupby cells with group values\n        /**\n         * Recursive function that generates the header cells corresponding to\n         * the groups of a given tree.\n         *\n         * @param {Object} tree\n         */\n        function generateTreeHeaders(tree, fields) {\n            const group = tree.root;\n            const rowIndex = group.values.length;\n            const row = colGroupRows[rowIndex];\n            const groupId = [[], group.values];\n            const isLeaf = !tree.directSubTrees.size;\n            const leafCount = leafCounts[JSON.stringify(tree.root.values)];\n            const cell = {\n                groupId: groupId,\n                height: isLeaf ? colGroupBys.length + 1 - rowIndex : 1,\n                isLeaf: isLeaf,\n                isFolded: isLeaf && colGroupBys.length > group.values.length,\n                label:\n                    rowIndex === 0\n                        ? undefined\n                        : fields[colGroupBys[rowIndex - 1].split(\":\")[0]].string,\n                title: group.labels.length ? group.labels[group.labels.length - 1] : _t(\"Total\"),\n                width: leafCount * measureCount * (2 * originCount - 1),\n            };\n            row.push(cell);\n            if (isLeaf) {\n                measureColumns.push(cell);\n            }\n\n            [...tree.directSubTrees.values()].forEach((subTree) => {\n                generateTreeHeaders(subTree, fields);\n            });\n        }\n\n        generateTreeHeaders(this.data.colGroupTree, this.metaData.fields);\n        // blank top right cell for 'Total' group (if there is more that one leaf)\n        if (leafCounts[JSON.stringify(this.data.colGroupTree.root.values)] > 1) {\n            var groupId = [[], []];\n            var totalTopRightCell = {\n                groupId: groupId,\n                height: height,\n                title: \"\",\n                width: measureCount * (2 * originCount - 1),\n            };\n            colGroupRows[0].push(totalTopRightCell);\n            measureColumns.push(totalTopRightCell);\n        }\n        headers = headers.concat(colGroupRows);\n\n        // 2) generate measures row\n        var measuresRow = this._getMeasuresRow(measureColumns);\n        headers.push(measuresRow);\n\n        // 3) generate origins row if more than one origin\n        if (originCount > 1) {\n            headers.push(this._getOriginsRow(measuresRow));\n        }\n\n        return headers;\n    }\n    /**\n     * Returns the list of body rows of the pivot table for a given tree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @param {Object[]} columns\n     * @returns {Object[]}\n     */\n    _getTableRows(tree, columns) {\n        let rows = [];\n        const group = tree.root;\n        const rowGroupId = [group.values, []];\n        const title = group.labels.length ? group.labels[group.labels.length - 1] : _t(\"Total\");\n        const indent = group.labels.length;\n        const isLeaf = !tree.directSubTrees.size;\n        const rowGroupBys = this.metaData.fullRowGroupBys;\n\n        const subGroupMeasurements = columns.map((column) => {\n            const colGroupId = column.groupId;\n            const groupIntersectionId = [rowGroupId[0], colGroupId[1]];\n            const measure = column.measure;\n            const originIndexes = column.originIndexes || [0];\n\n            const value = this._getCellValue(groupIntersectionId, measure, originIndexes, {\n                data: this.data,\n            });\n\n            const measurement = {\n                groupId: groupIntersectionId,\n                originIndexes: originIndexes,\n                measure: measure,\n                value: value,\n                isBold: !groupIntersectionId[0].length || !groupIntersectionId[1].length,\n            };\n            return measurement;\n        });\n\n        rows.push({\n            title: title,\n            label:\n                indent === 0\n                    ? undefined\n                    : this.metaData.fields[rowGroupBys[indent - 1].split(\":\")[0]].string,\n            groupId: rowGroupId,\n            indent: indent,\n            isLeaf: isLeaf,\n            isFolded: isLeaf && rowGroupBys.length > group.values.length,\n            subGroupMeasurements: subGroupMeasurements,\n        });\n\n        const subTreeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n        subTreeKeys.forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            rows = rows.concat(this._getTableRows(subTree, columns));\n        });\n\n        return rows;\n    }\n    /**\n     * returns the height of a given groupTree\n     *\n     * @protected\n     * @param {Object} tree, a groupTree\n     * @returns {number}\n     */\n    _getTreeHeight(tree) {\n        const subTreeHeights = [...tree.directSubTrees.values()].map(\n            this._getTreeHeight.bind(this)\n        );\n        return Math.max(0, Math.max.apply(null, subTreeHeights)) + 1;\n    }\n    /**\n     * @protected\n     * @param {Data} data\n     * @returns {boolean} true iff there's no data in the table\n     */\n    _hasData(data) {\n        return (data.counts[JSON.stringify([[], []])] || []).some((count) => {\n            return count > 0;\n        });\n    }\n    /**\n     * Initialize/Reinitialize data.rowGroupTree, colGroupTree, measurements,\n     * counts and subdivide the group 'Total' as many times it is necessary.\n     * A first subdivision with no groupBy (divisors.slice(0, 1)) is made in\n     * order to see if there is data in the intersection of the group 'Total'\n     * and the various origins. In case there is none, non supplementary rpc\n     * will be done (see the code of subdivideGroup).\n     *\n     * @protected\n     * @param {Config} config\n     */\n    async _loadData(config, prune = true) {\n        config.data = {}; // data will be completely recomputed\n        const { data, metaData } = config;\n        data.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };\n        data.colGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };\n        data.measurements = {};\n        data.counts = {};\n        data.groupDomains = {};\n        data.numbering = {};\n        const key = JSON.stringify([[], []]);\n        data.groupDomains[key] = metaData.domains.slice(0);\n\n        const group = { rowValues: [], colValues: [] };\n        const leftDivisors = sections(metaData.fullRowGroupBys);\n        const rightDivisors = sections(metaData.fullColGroupBys);\n        const divisors = cartesian(leftDivisors, rightDivisors);\n\n        await this._subdivideGroup(group, divisors.slice(0, 1), config);\n        await this._subdivideGroup(group, divisors.slice(1), config);\n\n        // keep folded groups folded after the reload if the structure of the table is the same\n        if (prune && this._hasData(data) && this._hasData(this.data)) {\n            if (\n                symmetricalDifference(metaData.rowGroupBys, this.metaData.rowGroupBys).length === 0\n            ) {\n                this._pruneTree(data.rowGroupTree, this.data.rowGroupTree);\n            }\n            if (\n                symmetricalDifference(metaData.colGroupBys, this.metaData.colGroupBys).length === 0\n            ) {\n                this._pruneTree(data.colGroupTree, this.data.colGroupTree);\n            }\n        }\n\n        this.data = config.data;\n        this.metaData = config.metaData;\n    }\n    /**\n     * @protected\n     * @param {string} gb\n     * @returns {string}\n     */\n    _normalize(gb) {\n        const [fieldName, interval] = gb.split(\":\");\n        const field = this.metaData.fields[fieldName];\n        if ([\"date\", \"datetime\"].includes(field.type)) {\n            return `${fieldName}:${interval || \"month\"}`;\n        } else {\n            return fieldName;\n        }\n    }\n    /**\n     * Extract the information in the read_group results (groupSubdivisions)\n     * and develop this.data.rowGroupTree, colGroupTree, measurements, counts, and\n     * groupDomains.\n     * If a column needs to be sorted, the rowGroupTree corresponding to the\n     * group is sorted.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Object[]} groupSubdivisions\n     * @param {Config} config\n     */\n    _prepareData(group, groupSubdivisions, config) {\n        const { data, metaData } = config;\n        const groupRowValues = group.rowValues;\n        let groupRowLabels = [];\n        let rowSubTree = data.rowGroupTree;\n        let root;\n        if (groupRowValues.length) {\n            // we should have labels information on hand! regretful!\n            rowSubTree = this._findGroup(data.rowGroupTree, groupRowValues);\n            root = rowSubTree.root;\n            groupRowLabels = root.labels;\n        }\n\n        const groupColValues = group.colValues;\n        let groupColLabels = [];\n        if (groupColValues.length) {\n            root = this._findGroup(data.colGroupTree, groupColValues).root;\n            groupColLabels = root.labels;\n        }\n\n        groupSubdivisions.forEach((groupSubdivision) => {\n            groupSubdivision.subGroups.forEach((subGroup) => {\n                const rowValues = groupRowValues.concat(\n                    this._getGroupValues(subGroup, groupSubdivision.rowGroupBy)\n                );\n                const rowLabels = groupRowLabels.concat(\n                    this._getGroupLabels(subGroup, groupSubdivision.rowGroupBy, config)\n                );\n\n                const colValues = groupColValues.concat(\n                    this._getGroupValues(subGroup, groupSubdivision.colGroupBy)\n                );\n                const colLabels = groupColLabels.concat(\n                    this._getGroupLabels(subGroup, groupSubdivision.colGroupBy, config)\n                );\n\n                if (!colValues.length && rowValues.length) {\n                    this._addGroup(data.rowGroupTree, rowLabels, rowValues);\n                }\n                if (colValues.length && !rowValues.length) {\n                    this._addGroup(data.colGroupTree, colLabels, colValues);\n                }\n\n                const key = JSON.stringify([rowValues, colValues]);\n                const originIndex = groupSubdivision.group.originIndex;\n\n                if (!(key in data.measurements)) {\n                    data.measurements[key] = metaData.origins.map(() => {\n                        return this._getMeasurements({}, config);\n                    });\n                }\n                data.measurements[key][originIndex] = this._getMeasurements(subGroup, config);\n\n                if (!(key in data.counts)) {\n                    data.counts[key] = metaData.origins.map(function () {\n                        return 0;\n                    });\n                }\n                data.counts[key][originIndex] = subGroup.__count;\n\n                if (!(key in data.groupDomains)) {\n                    data.groupDomains[key] = metaData.origins.map(function () {\n                        return Domain.FALSE.toList();\n                    });\n                }\n                // if __domain is not defined this means that we are in the\n                // case where\n                // groupSubdivision.rowGroupBy = groupSubdivision.rowGroupBy = []\n                if (subGroup.__domain) {\n                    data.groupDomains[key][originIndex] = subGroup.__domain;\n                }\n            });\n        });\n\n        if (metaData.sortedColumn) {\n            this._sortRows(metaData.sortedColumn, config);\n        }\n    }\n    /**\n     * Make any group in tree a leaf if it was a leaf in oldTree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @param {Object} oldTree\n     */\n    _pruneTree(tree, oldTree) {\n        if (!oldTree.directSubTrees.size) {\n            tree.directSubTrees.clear();\n            delete tree.sortedKeys;\n            return;\n        }\n        [...tree.directSubTrees.keys()].forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            if (!oldTree.directSubTrees.has(subTreeKey)) {\n                subTree.directSubTrees.clear();\n                delete subTree.sortedKeys;\n            } else {\n                const oldSubTree = oldTree.directSubTrees.get(subTreeKey);\n                this._pruneTree(subTree, oldSubTree);\n            }\n        });\n    }\n\n    _getEmptyGroupLabel(fieldName) {\n        return _t(\"None\");\n    }\n\n    /**\n     * Extract from a groupBy value a label.\n     *\n     * @protected\n     * @param {any} value\n     * @param {string} groupBy\n     * @param {Config} config\n     * @returns {string}\n     */\n    _sanitizeLabel(value, groupBy, config) {\n        const { metaData } = config;\n        const fieldName = groupBy.split(\":\")[0];\n        if (\n            fieldName &&\n            metaData.fields[fieldName] &&\n            metaData.fields[fieldName].type === \"boolean\"\n        ) {\n            return value === undefined ? _t(\"None\") : value ? _t(\"Yes\") : _t(\"No\");\n        }\n        if (value === false) {\n            return this._getEmptyGroupLabel(fieldName);\n        }\n        if (value instanceof Array) {\n            return this._getNumberedLabel(value, fieldName, config);\n        }\n        if (\n            fieldName &&\n            metaData.fields[fieldName] &&\n            metaData.fields[fieldName].type === \"selection\"\n        ) {\n            const selected = metaData.fields[fieldName].selection.find((o) => o[0] === value);\n            return selected ? selected[1] : value; // selected should be truthy normally ?!\n        }\n        return value;\n    }\n    /**\n     * Extract from a groupBy value the raw value of that groupBy (discarding\n     * a label if any)\n     *\n     * @protected\n     * @param {any} value\n     * @returns {any}\n     */\n    _sanitizeValue(value) {\n        if (value instanceof Array) {\n            return value[0];\n        }\n        return value;\n    }\n    /**\n     * Get all partitions of a given group using the provided list of divisors\n     * and enrich the objects of this.data.rowGroupTree, colGroupTree,\n     * measurements, counts.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Array[]} divisors\n     * @param {Config} config\n     */\n    async _subdivideGroup(group, divisors, config) {\n        const { data, metaData } = config;\n        const key = JSON.stringify([group.rowValues, group.colValues]);\n\n        const proms = metaData.origins.reduce((acc, origin, originIndex) => {\n            // if no information on group content is available, we fetch data.\n            // if group is known to be empty for the given origin,\n            // we don't need to fetch data for that origin.\n            if (!data.counts[key] || data.counts[key][originIndex] > 0) {\n                const subGroup = {\n                    rowValues: group.rowValues,\n                    colValues: group.colValues,\n                    originIndex: originIndex,\n                };\n                const groupDomain = this._getGroupDomain(subGroup, config);\n                const measureSpecs = this._getMeasureSpecs(config);\n                const resModel = config.metaData.resModel;\n                const kwargs = { lazy: false, context: this.searchParams.context };\n                const mapping = {};\n                divisors.forEach((divisor) => {\n                    acc.push(\n                        this._getGroupSubdivision(subGroup, divisor[0], divisor[1], {\n                            resModel,\n                            groupDomain,\n                            measureSpecs,\n                            kwargs,\n                            mapping,\n                        })\n                    );\n                });\n            }\n            return acc;\n        }, []);\n        const groupSubdivisions = await this.keepLast.add(Promise.all(proms));\n        if (groupSubdivisions.length) {\n            this._prepareData(group, groupSubdivisions, config);\n        }\n    }\n    /**\n     * Sort the rows, depending on the values of a given column.  This is an\n     * in-memory sort.\n     *\n     * @protected\n     * @param {Object} sortedColumn\n     * @param {number[]} sortedColumn.groupId\n     * @param {Config} config\n     */\n    _sortRows(sortedColumn, config) {\n        const metaData = config.metaData || this.metaData;\n        const data = config.data || this.data;\n        const colGroupValues = sortedColumn.groupId[1];\n        sortedColumn.originIndexes = sortedColumn.originIndexes || [0];\n        metaData.sortedColumn = sortedColumn;\n\n        const sortFunction = (tree) => {\n            return (subTreeKey) => {\n                const subTree = tree.directSubTrees.get(subTreeKey);\n                const groupIntersectionId = [subTree.root.values, colGroupValues];\n                const value =\n                    this._getCellValue(\n                        groupIntersectionId,\n                        sortedColumn.measure,\n                        sortedColumn.originIndexes,\n                        { data }\n                    ) || 0;\n                return sortedColumn.order === \"asc\" ? value : -value;\n            };\n        };\n\n        this._sortTree(sortFunction, data.rowGroupTree);\n    }\n    /**\n     * Sort recursively the subTrees of tree using sortFunction.\n     * In the end each node of the tree has its direct children sorted\n     * according to the criterion reprensented by sortFunction.\n     *\n     * @protected\n     * @param {Function} sortFunction\n     * @param {Object} tree\n     */\n    _sortTree(sortFunction, tree) {\n        tree.sortedKeys = sortBy([...tree.directSubTrees.keys()], sortFunction(tree));\n        [...tree.directSubTrees.values()].forEach((subTree) => {\n            this._sortTree(sortFunction, subTree);\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatPercentage } from \"@web/views/fields/formatters\";\nimport { PivotHeader } from \"@web/views/pivot/pivot_header\";\n\nimport { Component, onWillUpdateProps, useRef } from \"@odoo/owl\";\nimport { download } from \"@web/core/network/download\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ReportViewMeasures } from \"@web/views/view_components/report_view_measures\";\n\nconst formatters = registry.category(\"formatters\");\n\nexport class PivotRenderer extends Component {\n    static template = \"web.PivotRenderer\";\n    static components = { Dropdown, DropdownItem, CheckBox, PivotHeader, ReportViewMeasures };\n    static props = [\"model\", \"buttonTemplate\"];\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.model = this.props.model;\n        this.table = this.model.getTable();\n        this.l10n = localization;\n        this.tableRef = useRef(\"table\");\n\n        onWillUpdateProps(this.onWillUpdateProps);\n    }\n    onWillUpdateProps() {\n        this.table = this.model.getTable();\n    }\n    /**\n     * Get the formatted value of the cell.\n     *\n     * @private\n     * @param {Object} cell\n     * @returns {string} Formatted value\n     */\n    getFormattedValue(cell) {\n        const field = this.model.metaData.measures[cell.measure];\n        let formatType = this.model.metaData.widgets[cell.measure];\n        if (!formatType) {\n            const fieldType = field.type;\n            formatType = [\"many2one\", \"reference\"].includes(fieldType) ? \"integer\" : fieldType;\n        }\n        const formatter = formatters.get(formatType);\n        return formatter(cell.value, field);\n    }\n    /**\n     * Get the formatted variation of a cell.\n     *\n     * @private\n     * @param {Object} cell\n     * @returns {string} Formatted variation\n     */\n    getFormattedVariation(cell) {\n        if (isNaN(cell.value)) {\n            return \"-\";\n        }\n        return formatPercentage(cell.value, this.model.metaData.fields[cell.measure]);\n    }\n\n    getHeaderProps({ cell, isXAxis = false, isInHead = false }) {\n        const type = isXAxis ? \"col\" : \"row\";\n        return {\n            cell,\n            isXAxis,\n            isInHead,\n            customGroupBys: this.model.metaData.customGroupBys,\n            onItemSelected: (payload) => this.onGroupBySelected(type, payload),\n            onAddCustomGroupBy: (fieldName) =>\n                this.onAddCustomGroupBy(type, cell.groupId, fieldName),\n            onClick: () => this.onHeaderClick(cell, type),\n        };\n    }\n\n    //----------------------------------------------------------------------\n    // Handlers\n    //----------------------------------------------------------------------\n\n    /**\n     * Handle the adding of a custom groupby (inside the view, not the searchview).\n     *\n     * @param {\"col\"|\"row\"} type\n     * @param {Array[]} groupId\n     * @param {string} fieldName\n     */\n    onAddCustomGroupBy(type, groupId, fieldName) {\n        this.model.addGroupBy({ groupId, fieldName, custom: true, type });\n    }\n\n    /**\n     * Handle the selection of a groupby dropdown item.\n     *\n     * @param {\"col\"|\"row\"} type\n     * @param {Object} payload\n     */\n    onGroupBySelected(type, payload) {\n        this.model.addGroupBy({ ...payload, type });\n    }\n    /**\n     * Handle a click on a header cell.\n     *\n     * @param {Object} cell\n     * @param {string} type col or row\n     */\n    onHeaderClick(cell, type) {\n        if (cell.isLeaf && cell.isFolded) {\n            this.model.expandGroup(cell.groupId, type);\n        } else if (!cell.isLeaf) {\n            this.model.closeGroup(cell.groupId, type);\n        }\n    }\n    /**\n     * Handle a click on a measure cell.\n     *\n     * @param {Object} cell\n     */\n    onMeasureClick(cell) {\n        this.model.sortRows({\n            groupId: cell.groupId,\n            measure: cell.measure,\n            order: (cell.order || \"desc\") === \"asc\" ? \"desc\" : \"asc\",\n            originIndexes: cell.originIndexes,\n        });\n    }\n    /**\n     * Hover the column in which the mouse is.\n     *\n     * @param {MouseEvent} ev\n     */\n    onMouseEnter(ev) {\n        var index = [...ev.currentTarget.parentNode.children].indexOf(ev.currentTarget);\n        if (ev.currentTarget.tagName === \"TH\") {\n            if (\n                !ev.currentTarget.classList.contains(\"o_pivot_origin_row\") &&\n                this.model.metaData.origins.length === 2\n            ) {\n                index = 3 * index; // two origins + comparison column\n            }\n            index += 1; // row groupbys column\n        }\n        this.tableRef.el\n            .querySelectorAll(\"td:nth-child(\" + (index + 1) + \")\")\n            .forEach((elt) => elt.classList.add(\"o_cell_hover\"));\n    }\n    /**\n     * Remove the hover on the columns.\n     */\n    onMouseLeave() {\n        this.tableRef.el\n            .querySelectorAll(\".o_cell_hover\")\n            .forEach((elt) => elt.classList.remove(\"o_cell_hover\"));\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Exports the current pivot table data in a xls file. For this, we have to\n     * serialize the current state, then call the server /web/pivot/export_xlsx.\n     * Force a reload before exporting to ensure to export up-to-date data.\n     */\n    onDownloadButtonClicked() {\n        if (this.model.getTableWidth() > 16384) {\n            throw new Error(\n                _t(\n                    \"For Excel compatibility, data cannot be exported if there are more than 16384 columns.\\n\\nTip: try to flip axis, filter further or reduce the number of measures.\"\n                )\n            );\n        }\n        const table = this.model.exportData();\n        download({\n            url: \"/web/pivot/export_xlsx\",\n            data: { data: new Blob([JSON.stringify(table)], { type: \"application/json\" }) },\n        });\n    }\n    /**\n     * Expands all groups\n     */\n    onExpandButtonClicked() {\n        this.model.expandAll();\n    }\n    /**\n     * Flips axis\n     */\n    onFlipButtonClicked() {\n        this.model.flip();\n    }\n    /**\n     * Toggles the given measure\n     *\n     * @param {Object} param0\n     * @param {string} param0.measure\n     */\n    onMeasureSelected({ measure }) {\n        this.model.toggleMeasure(measure);\n    }\n    /**\n     * Execute the action to open the view on the current model.\n     *\n     * @param {Array} domain\n     * @param {Array} views\n     * @param {Object} context\n     */\n    openView(domain, views, context) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: this.model.metaData.title,\n            res_model: this.model.metaData.resModel,\n            views: views,\n            view_mode: \"list\",\n            target: \"current\",\n            context,\n            domain,\n        });\n    }\n    /**\n     * @param {CustomEvent} ev\n     */\n    onOpenView(cell) {\n        if (cell.value === undefined || this.model.metaData.disableLinking) {\n            return;\n        }\n\n        const context = Object.assign({}, this.model.searchParams.context);\n        Object.keys(context).forEach((x) => {\n            if (x === \"group_by\" || x.startsWith(\"search_default_\")) {\n                delete context[x];\n            }\n        });\n\n        // retrieve form and list view ids from the action\n        const { views = [] } = this.env.config;\n        this.views = [\"list\", \"form\"].map((viewType) => {\n            const view = views.find((view) => view[1] === viewType);\n            return [view ? view[0] : false, viewType];\n        });\n\n        const group = {\n            rowValues: cell.groupId[0],\n            colValues: cell.groupId[1],\n            originIndex: cell.originIndexes[0],\n        };\n        this.openView(this.model.getGroupDomain(group), this.views, context);\n    }\n}\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport class PivotSearchModel extends SearchModel {\n    _getIrFilterDescription() {\n        this.preparingIrFilterDescription = true;\n        const result = super._getIrFilterDescription(...arguments);\n        this.preparingIrFilterDescription = false;\n        return result;\n    }\n\n    _getSearchItemGroupBys(activeItem) {\n        const { searchItemId } = activeItem;\n        const { context, type } = this.searchItems[searchItemId];\n        if (\n            !this.preparingIrFilterDescription &&\n            type === \"favorite\" &&\n            context.pivot_row_groupby\n        ) {\n            return context.pivot_row_groupby;\n        }\n        return super._getSearchItemGroupBys(...arguments);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { PivotArchParser } from \"@web/views/pivot/pivot_arch_parser\";\nimport { PivotController } from \"./pivot_controller\";\nimport { PivotModel } from \"@web/views/pivot/pivot_model\";\nimport { PivotRenderer } from \"@web/views/pivot/pivot_renderer\";\nimport { PivotSearchModel } from \"./pivot_search_model\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const pivotView = {\n    type: \"pivot\",\n    Controller: PivotController,\n    Renderer: PivotRenderer,\n    Model: PivotModel,\n    ArchParser: PivotArchParser,\n    SearchModel: PivotSearchModel,\n    searchMenuTypes: [\"filter\", \"groupBy\", \"comparison\", \"favorite\"],\n    buttonTemplate: \"web.PivotView.Buttons\",\n\n    props: (genericProps, view) => {\n        const modelParams = {};\n        if (genericProps.state) {\n            modelParams.data = genericProps.state.data;\n            modelParams.metaData = genericProps.state.metaData;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n\n            // parse arch\n            const archInfo = new view.ArchParser().parse(arch);\n\n            if (!archInfo.activeMeasures.length || archInfo.displayQuantity) {\n                archInfo.activeMeasures.unshift(\"__count\");\n            }\n\n            modelParams.metaData = {\n                activeMeasures: archInfo.activeMeasures,\n                colGroupBys: archInfo.colGroupBys,\n                defaultOrder: archInfo.defaultOrder,\n                disableLinking: Boolean(archInfo.disableLinking),\n                fields: fields,\n                fieldAttrs: archInfo.fieldAttrs,\n                resModel: resModel,\n                rowGroupBys: archInfo.rowGroupBys,\n                title: archInfo.title || _t(\"Untitled\"),\n                widgets: archInfo.widgets,\n            };\n        }\n\n        return {\n            ...genericProps,\n            Model: view.Model,\n            modelParams,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nviewRegistry.add(\"pivot\", pivotView);\n", "import { visitXML } from \"@web/core/utils/xml\";\nimport { Field } from \"@web/views/fields/field\";\n\nexport class ActivityArchParser {\n    parse(xmlDoc, models, modelName) {\n        const jsClass = xmlDoc.getAttribute(\"js_class\");\n        const title = xmlDoc.getAttribute(\"string\");\n\n        const fieldNodes = {};\n        const templateDocs = {};\n        const fieldNextIds = {};\n\n        visitXML(xmlDoc, (node) => {\n            if (node.hasAttribute(\"t-name\")) {\n                templateDocs[node.getAttribute(\"t-name\")] = node;\n                return;\n            }\n\n            if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(\n                    node,\n                    models,\n                    modelName,\n                    \"activity\",\n                    jsClass\n                );\n                if (!(fieldInfo.name in fieldNextIds)) {\n                    fieldNextIds[fieldInfo.name] = 0;\n                }\n                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;\n                fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n            }\n\n            // Keep track of last update so images can be reloaded when they may have changed.\n            if (node.tagName === \"img\") {\n                const attSrc = node.getAttribute(\"t-att-src\");\n                if (\n                    attSrc &&\n                    /\\bactivity_image\\b/.test(attSrc) &&\n                    !Object.values(fieldNodes).some((f) => f.name === \"write_date\")\n                ) {\n                    fieldNodes.write_date_0 = { name: \"write_date\", type: \"datetime\" };\n                }\n            }\n        });\n        return {\n            fieldNodes,\n            templateDocs,\n            title,\n        };\n    }\n}\n", "import { ActivityListPopover } from \"@mail/core/web/activity_list_popover\";\nimport { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nimport { formatDate } from \"@web/core/l10n/dates\";\n\nexport class ActivityCell extends Component {\n    static components = {\n        Avatar,\n    };\n    static props = {\n        activityIds: {\n            type: Array,\n            elements: Number,\n        },\n        attachmentsInfo: {\n            optional: true,\n            type: Object,\n        },\n        activityTypeId: Number,\n        reportingDate: String,\n        countByState: Object,\n        reloadFunc: Function,\n        resId: Number,\n        resModel: String,\n        userAssignedIds: Array,\n    };\n    static template = \"mail.ActivityCell\";\n\n    setup() {\n        this.popover = usePopover(ActivityListPopover, { position: \"bottom-start\" });\n        this.contentRef = useRef(\"content\");\n    }\n\n    get reportingDateFormatted() {\n        return formatDate(luxon.DateTime.fromISO(this.props.reportingDate));\n    }\n\n    get ongoingActivityCount() {\n        return (\n            (this.props.countByState?.planned ?? 0) +\n            (this.props.countByState?.today ?? 0) +\n            (this.props.countByState?.overdue ?? 0)\n        );\n    }\n\n    get totalActivityCount() {\n        return this.ongoingActivityCount + (this.props.countByState?.done ?? 0);\n    }\n\n    onClick() {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            this.popover.open(this.contentRef.el, {\n                activityIds: this.props.activityIds,\n                defaultActivityTypeId: this.props.activityTypeId,\n                onActivityChanged: () => {\n                    this.props.reloadFunc();\n                    this.popover.close();\n                },\n                resId: this.props.resId,\n                resModel: this.props.resModel,\n            });\n        }\n    }\n}\n", "import { createElement, extractAttributes } from \"@web/core/utils/xml\";\nimport { toInterpolatedStringExpression, ViewCompiler } from \"@web/views/view_compiler\";\nimport { toStringExpression } from \"@web/views/utils\";\n\nexport class ActivityCompiler extends ViewCompiler {\n    /**\n     * @override\n     */\n    compileField(el, params) {\n        let compiled;\n        if (el.hasAttribute(\"widget\")) {\n            compiled = super.compileField(el, params);\n        } else {\n            // fields without a specified widget are rendered as simple spans in activity records\n            compiled = createElement(\"div\", {\n                \"t-out\": `record[\"${el.getAttribute(\"name\")}\"].value`,\n            });\n        }\n        const classNames = [];\n        const { bold, display, muted } = extractAttributes(el, [\"bold\", \"display\", \"muted\"]);\n        if (display === \"right\") {\n            classNames.push(\"float-end\");\n        }\n        if (display === \"full\") {\n            classNames.push(\"d-block\", \"text-truncate\");\n        } else {\n            classNames.push(\"d-inline-block\");\n        }\n        if (bold) {\n            classNames.push(\"fw-bold\");\n        }\n        if (muted) {\n            classNames.push(\"text-muted\");\n        }\n        if (classNames.length > 0) {\n            const clsFormatted = el.hasAttribute(\"widget\")\n                ? toStringExpression(classNames.join(\" \"))\n                : classNames.join(\" \");\n            compiled.setAttribute(\"class\", clsFormatted);\n        }\n\n        const attrs = {};\n        for (const attr of el.attributes) {\n            attrs[attr.name] = attr.value;\n        }\n\n        if (el.hasAttribute(\"widget\")) {\n            const attrsParts = Object.entries(attrs).map(([key, value]) => {\n                if (key.startsWith(\"t-attf-\")) {\n                    key = key.slice(7);\n                    value = toInterpolatedStringExpression(value);\n                } else if (key.startsWith(\"t-att-\")) {\n                    key = key.slice(6);\n                    value = `\"\" + (${value})`;\n                } else if (key.startsWith(\"t-att\")) {\n                    throw new Error(\"t-att on <field> nodes is not supported\");\n                } else if (!key.startsWith(\"t-\")) {\n                    value = toStringExpression(value);\n                }\n                return `'${key}':${value}`;\n            });\n            compiled.setAttribute(\"attrs\", `{${attrsParts.join(\",\")}}`);\n        }\n\n        for (const attr in attrs) {\n            if (attr.startsWith(\"t-\") && !attr.startsWith(\"t-att\")) {\n                compiled.setAttribute(attr, attrs[attr]);\n            }\n        }\n\n        return compiled;\n    }\n}\n\nActivityCompiler.OWL_DIRECTIVE_WHITELIST = [\n    ...ViewCompiler.OWL_DIRECTIVE_WHITELIST,\n    \"t-name\",\n    \"t-esc\",\n    \"t-out\",\n    \"t-set\",\n    \"t-value\",\n    \"t-if\",\n    \"t-else\",\n    \"t-elif\",\n    \"t-foreach\",\n    \"t-as\",\n    \"t-key\",\n    \"t-att.*\",\n    \"t-call\",\n    \"t-translation\",\n];\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useModel } from \"@web/model/model\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { Layout } from \"@web/search/layout\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\nexport class ActivityController extends Component {\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        archInfo: Object,\n    };\n    static template = \"mail.ActivityController\";\n\n    setup() {\n        this.model = useState(useModel(this.props.Model, this.modelParams));\n\n        this.dialog = useService(\"dialog\");\n        this.action = useService(\"action\");\n        this.store = useService(\"mail.store\");\n        this.ui = useState(useService(\"ui\"));\n        usePager(() => {\n            const { count, hasLimitedCount, limit, offset } = this.model.root;\n            return {\n                offset: offset,\n                limit: limit,\n                total: count,\n                onUpdate: async (params) => {\n                    // Ensure that only (active) records with at least one activity, \"done\" (archived) or not, are fetched.\n                    // We don't use active_test=false in the context because otherwise we would also get archived records.\n                    params.domain = [...(this.model.originalDomain || []), [\"activity_ids.active\", \"in\", [true, false]]];\n                    await Promise.all([\n                        this.model.root.load(params),\n                        this.model.fetchActivityData(params),\n                    ]);\n                },\n                updateTotal: hasLimitedCount ? () => this.model.root.fetchCount() : undefined,\n            };\n        });\n    }\n\n    get modelParams() {\n        const { archInfo, resModel } = this.props;\n        const { activeFields, fields } = extractFieldsFromArchInfo(archInfo, this.props.fields);\n        return {\n            config: {\n                activeFields,\n                resModel,\n                fields,\n            },\n        };\n    }\n\n    getSearchProps() {\n        const { comparision, context, domain, groupBy, orderBy } = this.env.searchModel;\n        return { comparision, context, domain, groupBy, orderBy };\n    }\n\n    scheduleActivity() {\n        this.dialog.add(SelectCreateDialog, {\n            resModel: this.props.resModel,\n            searchViewId: this.env.searchModel.searchViewId,\n            domain: this.model.originalDomain,\n            title: _t(\"Search: %s\", this.props.archInfo.title),\n            multiSelect: false,\n            context: this.props.context,\n            noCreate: this.props.context?.create === false,\n            onSelected: async (resIds) => {\n                await this.store.scheduleActivity(this.props.resModel, resIds);\n            },\n        }, {\n            onClose: () => this.model.load(this.getSearchProps())\n        });\n    }\n\n    openActivityFormView(resId, activityTypeId) {\n        this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"mail.activity\",\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                view_type: \"form\",\n                res_id: false,\n                target: \"new\",\n                context: {\n                    default_res_id: resId,\n                    default_res_model: this.props.resModel,\n                    default_activity_type_id: activityTypeId,\n                },\n            },\n            {\n                onClose: () => this.model.load(this.getSearchProps()),\n            }\n        );\n    }\n\n    sendMailTemplate(templateID, activityTypeID) {\n        const groupedActivities = this.model.activityData.grouped_activities;\n        const resIds = [];\n        for (const resId in groupedActivities) {\n            const activityByType = groupedActivities[resId];\n            const activity = activityByType[activityTypeID];\n            if (activity) {\n                resIds.push(parseInt(resId));\n            }\n        }\n        this.model.orm.call(this.props.resModel, \"activity_send_mail\", [resIds, templateID], {});\n    }\n\n    async openRecord(record, mode) {\n        const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);\n        this.props.selectRecord(record.resId, { activeIds, mode });\n    }\n\n    get rendererProps() {\n        return {\n            activityTypes: this.model.activityData.activity_types,\n            activityResIds: this.model.activityData.activity_res_ids,\n            fields: this.model.root.fields,\n            records: this.model.root.records,\n            resModel: this.props.resModel,\n            archInfo: this.props.archInfo,\n            groupedActivities: this.model.activityData.grouped_activities,\n            scheduleActivity: this.scheduleActivity.bind(this),\n            onReloadData: () => this.model.load(this.getSearchProps()),\n            onEmptyCell: this.openActivityFormView.bind(this),\n            onSendMailTemplate: this.sendMailTemplate.bind(this),\n            openRecord: this.openRecord.bind(this),\n        };\n    }\n}\n", "import { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nexport class ActivityModel extends RelationalModel {\n    static DEFAULT_LIMIT = 100;\n\n    async load(params = {}) {\n        this.originalDomain = params.domain ? [...params.domain] : [];\n        // Ensure that only (active) records with at least one activity, \"done\" (archived) or not, are fetched.\n        // We don't use active_test=false in the context because otherwise we would also get archived records.\n        params.domain = [...(params.domain || []), [\"activity_ids.active\", \"in\", [true, false]]];\n        if (params && \"groupBy\" in params) {\n            params.groupBy = [];\n        }\n        await Promise.all([this.fetchActivityData(params), super.load(params)]);\n    }\n\n    async fetchActivityData(params) {\n        this.activityData = await this.orm.call(\"mail.activity\", \"get_activity_data\", [], {\n            res_model: this.config.resModel,\n            domain: params.domain || this.env.searchModel._domain,\n            limit: params.limit || this.initialLimit,\n            offset: params.offset || 0,\n            fetch_done: true,\n        });\n    }\n}\n", "import { ActivityCompiler } from \"@mail/views/web/activity/activity_compiler\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { isHtmlEmpty } from \"@web/core/utils/html\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getFormattedRecord, getImageSrcFromRecordInfo } from \"@web/views/kanban/kanban_record\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\n\nexport class ActivityRecord extends Component {\n    static components = {\n        Field,\n    };\n    static props = {\n        archInfo: { type: Object },\n        openRecord: { type: Function },\n        record: { type: Object },\n    };\n    static template = \"mail.ActivityRecord\";\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        this.widget = {\n            deletable: false,\n            editable: false,\n            isHtmlEmpty,\n        };\n        const { templateDocs } = this.props.archInfo;\n        const templates = useViewCompiler(ActivityCompiler, templateDocs);\n        this.recordTemplate = templates[\"activity-box\"];\n    }\n\n    getRenderingContext() {\n        const { record } = this.props;\n        return {\n            record: getFormattedRecord(record),\n            activity_image: (...args) => getImageSrcFromRecordInfo(record, ...args),\n            user_context: user.context,\n            widget: this.widget,\n            luxon,\n            __comp__: Object.assign(Object.create(this), { this: this }),\n        };\n    }\n}\n", "import { MailColumnProgress } from \"@mail/core/web/mail_column_progress\";\nimport { ActivityCell } from \"@mail/views/web/activity/activity_cell\";\nimport { ActivityRecord } from \"@mail/views/web/activity/activity_record\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ActivityRenderer extends Component {\n    static components = {\n        ActivityCell,\n        ActivityRecord,\n        ColumnProgress: MailColumnProgress,\n        Dropdown,\n        DropdownItem,\n        CheckBox,\n    };\n    static props = {\n        activityTypes: { type: Object },\n        activityResIds: { type: Array },\n        fields: { type: Object },\n        resModel: { type: String },\n        records: { type: Array },\n        archInfo: { type: Object },\n        groupedActivities: { type: Object },\n        scheduleActivity: { type: Function },\n        onReloadData: { type: Function },\n        onEmptyCell: { type: Function },\n        onSendMailTemplate: { type: Function },\n        openRecord: { type: Function },\n    };\n    static template = \"mail.ActivityRenderer\";\n\n    setup() {\n        this.activeFilter = useState({\n            progressValue: {\n                active: null,\n            },\n            activityTypeId: null,\n            resIds: new Set(Object.keys(this.props.groupedActivities)),\n        });\n\n        this.storageKey = [\"activity_columns\", this.props.resModel, this.env.config.viewId];\n        this.setupStorageActiveColumns();\n    }\n\n    getGroupInfo(activityType) {\n        const types = {\n            done: {\n                color: \"secondary\",\n                inProgressBar: false,\n                label: _t(\"done\"), // activity_mixin.activity_state has no done state, so we add it manually here\n                value: 0,\n            },\n            planned: {\n                color: \"success\",\n                inProgressBar: true,\n                value: 0,\n            },\n            today: {\n                color: \"warning\",\n                inProgressBar: true,\n                value: 0,\n            },\n            overdue: {\n                color: \"danger\",\n                inProgressBar: true,\n                value: 0,\n            },\n        };\n        for (const [type, label] of this.props.fields.activity_state.selection) {\n            types[type].label = label;\n        }\n        const typeId = activityType.id;\n        const isColumnFiltered = this.activeFilter.activityTypeId === activityType.id;\n        const progressValue = isColumnFiltered ? this.activeFilter.progressValue : { active: null };\n\n        let totalCountWithoutDone = 0;\n        for (const activities of Object.values(this.props.groupedActivities)) {\n            if (typeId in activities) {\n                for (const [state, stateCount] of Object.entries(\n                    activities[typeId].count_by_state\n                )) {\n                    types[state].value += stateCount;\n                    if (state !== \"done\") {\n                        totalCountWithoutDone += stateCount;\n                    }\n                }\n            }\n        }\n\n        const progressBar = {\n            bars: [],\n            activeBar: isColumnFiltered ? this.activeFilter.progressValue.active : null,\n        };\n        for (const [value, count] of Object.entries(types)) {\n            if (count.inProgressBar) {\n                progressBar.bars.push({\n                    count: count.value,\n                    value,\n                    string: types[value].label,\n                    color: count.color,\n                });\n            }\n        }\n\n        const ongoingActivityCount = types.overdue.value + types.today.value + types.planned.value;\n        const ongoingAndDoneCount = ongoingActivityCount + types.done.value;\n        const labelAggregate = `${types.overdue.label} + ${types.today.label} + ${types.planned.label}`;\n        const aggregateOn =\n            ongoingAndDoneCount && this.isTypeDisplayDone(typeId)\n                ? {\n                      title: `${types.done.label} + ${labelAggregate}`,\n                      value: ongoingAndDoneCount,\n                  }\n                : undefined;\n        return {\n            aggregate: {\n                title: labelAggregate,\n                value: isColumnFiltered ? types[progressValue.active].value : ongoingActivityCount,\n            },\n            aggregateOn: aggregateOn,\n            data: {\n                count: totalCountWithoutDone,\n                filterProgressValue: (name) => this.onSetProgressBarState(typeId, name),\n                progressBar,\n                progressValue,\n            },\n        };\n    }\n\n    getRecord(resId) {\n        return this.props.records.find((r) => r.resId === resId);\n    }\n\n    isTypeDisplayDone(typeId) {\n        return this.props.activityTypes.find((a) => a.id === typeId).keep_done;\n    }\n\n    onSetProgressBarState(typeId, bar) {\n        const name = bar.value;\n        if (this.activeFilter.progressValue.active === name) {\n            this.activeFilter.progressValue.active = null;\n            this.activeFilter.activityTypeId = null;\n            this.activeFilter.resIds = new Set(Object.keys(this.props.groupedActivities));\n        } else {\n            this.activeFilter.progressValue.active = name;\n            this.activeFilter.activityTypeId = typeId;\n            this.activeFilter.resIds = new Set(\n                Object.entries(this.props.groupedActivities)\n                    .filter(\n                        ([, resIds]) => typeId in resIds && name in resIds[typeId].count_by_state\n                    )\n                    .map(([key]) => parseInt(key))\n            );\n        }\n    }\n\n    get activeColumns() {\n        return this.props.activityTypes.filter(\n            (activityType) => this.storageActiveColumns[activityType.id]\n        );\n    }\n\n    setupStorageActiveColumns() {\n        const storageActiveColumnsList = browser.localStorage.getItem(this.storageKey)?.split(\",\");\n\n        this.storageActiveColumns = useState({});\n        for (const activityType of this.props.activityTypes) {\n            if (storageActiveColumnsList) {\n                this.storageActiveColumns[activityType.id] = storageActiveColumnsList.includes(\n                    activityType.id.toString()\n                );\n            } else {\n                this.storageActiveColumns[activityType.id] = true;\n            }\n        }\n    }\n\n    toggleDisplayColumn(typeId) {\n        this.storageActiveColumns[typeId] = !this.storageActiveColumns[typeId];\n        browser.localStorage.setItem(\n            this.storageKey.join(\",\"),\n            Object.keys(this.storageActiveColumns).filter(\n                (activityType) => this.storageActiveColumns[activityType]\n            )\n        );\n    }\n}\n", "import { ActivityArchParser } from \"@mail/views/web/activity/activity_arch_parser\";\nimport { ActivityController } from \"@mail/views/web/activity/activity_controller\";\nimport { ActivityModel } from \"@mail/views/web/activity/activity_model\";\nimport { ActivityRenderer } from \"@mail/views/web/activity/activity_renderer\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport const activityView = {\n    type: \"activity\",\n    searchMenuTypes: [\"filter\", \"favorite\"],\n    Controller: ActivityController,\n    Renderer: ActivityRenderer,\n    ArchParser: ActivityArchParser,\n    Model: ActivityModel,\n    props: (genericProps, view) => {\n        const { arch, relatedModels, resModel } = genericProps;\n        const archInfo = new view.ArchParser().parse(arch, relatedModels, resModel);\n        return {\n            ...genericProps,\n            archInfo,\n            Model: view.Model,\n            Renderer: view.Renderer,\n        };\n    },\n};\nregistry.category(\"views\").add(\"activity\", activityView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\nimport { ForecastSearchModel } from \"@crm/views/forecast_search_model\";\n\nexport const forecastGraphView = {\n    ...graphView,\n    SearchModel: ForecastSearchModel,\n};\n\nregistry.category(\"views\").add(\"forecast_graph\", forecastGraphView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\nimport { ForecastSearchModel } from \"@crm/views/forecast_search_model\";\n\nexport const forecastPivotView = {\n    ...pivotView,\n    SearchModel: ForecastSearchModel,\n};\n\nregistry.category(\"views\").add(\"forecast_pivot\", forecastPivotView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { GraphRenderer } from \"@web/views/graph/graph_renderer\";\nimport { graphView } from \"@web/views/graph/graph_view\";\n\nexport class StockForecastedGraphRenderer extends GraphRenderer {\n    static template = \"stock.ForecastedGraphRenderer\";\n};\n\nexport const StockForecastedGraphView = {\n    ...graphView,\n    Renderer: StockForecastedGraphRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_forecasted_graph\", StockForecastedGraphView);\n", "/** @odoo-module */\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { PivotRenderer } from \"@web/views/pivot/pivot_renderer\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\npatch(PivotRenderer.prototype, {\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        if (this.env.isSmall) {\n            useEffect(() => {\n                if (this.root.el) {\n                    const tooltipElems = this.root.el.querySelectorAll(\"*[data-tooltip]\");\n                    for (const el of tooltipElems) {\n                        el.removeAttribute(\"data-tooltip\");\n                        el.removeAttribute(\"data-tooltip-position\");\n                    }\n                }\n            });\n        }\n    },\n\n    getPadding(cell) {\n        if (this.env.isSmall) {\n            return 5 + cell.indent * 5;\n        }\n        return super.getPadding(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { unique } from \"@web/core/utils/arrays\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\n\nexport class MapArchParser {\n    parse(arch) {\n        const archInfo = {\n            fieldNames: [],\n            fieldNamesMarkerPopup: [],\n        };\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"map\":\n                    this.visitMap(node, archInfo);\n                    break;\n                case \"field\":\n                    this.visitField(node, archInfo);\n                    break;\n            }\n        });\n\n        archInfo.fieldNames = unique(archInfo.fieldNames);\n        archInfo.fieldNamesMarkerPopup = unique(archInfo.fieldNamesMarkerPopup);\n\n        return archInfo;\n    }\n\n    visitMap(node, archInfo) {\n        archInfo.resPartnerField = node.getAttribute(\"res_partner\");\n        archInfo.fieldNames.push(archInfo.resPartnerField);\n\n        if (node.hasAttribute(\"limit\")) {\n            archInfo.limit = parseInt(node.getAttribute(\"limit\"), 10);\n        }\n        if (node.hasAttribute(\"panel_title\")) {\n            archInfo.panelTitle = node.getAttribute(\"panel_title\");\n        }\n        if (node.hasAttribute(\"routing\")) {\n            archInfo.routing = exprToBoolean(node.getAttribute(\"routing\"));\n        }\n        if (node.hasAttribute(\"hide_title\")) {\n            archInfo.hideTitle = exprToBoolean(node.getAttribute(\"hide_title\"));\n        }\n        if (node.hasAttribute(\"hide_address\")) {\n            archInfo.hideAddress = exprToBoolean(node.getAttribute(\"hide_address\"));\n        }\n        if (node.hasAttribute(\"hide_name\")) {\n            archInfo.hideName = exprToBoolean(node.getAttribute(\"hide_name\"));\n        }\n        if (!archInfo.hideName) {\n            archInfo.fieldNames.push(\"display_name\");\n        }\n        if (node.hasAttribute(\"default_order\")) {\n            archInfo.defaultOrder = {\n                name: node.getAttribute(\"default_order\"),\n                asc: true,\n            };\n        }\n        if (node.hasAttribute(\"allow_resequence\")) {\n            archInfo.allowResequence = exprToBoolean(node.getAttribute(\"allow_resequence\"));\n        }\n    }\n    visitField(node, params) {\n        params.fieldNames.push(node.getAttribute(\"name\"));\n        params.fieldNamesMarkerPopup.push({\n            fieldName: node.getAttribute(\"name\"),\n            string: node.getAttribute(\"string\"),\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { loadJS, loadCSS } from \"@web/core/assets\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, onWillUnmount, onWillStart } from \"@odoo/owl\";\n\nexport class MapController extends Component {\n    static template = \"web_map.MapView\";\n    static components = {\n        Layout,\n        SearchBar,\n        CogMenu,\n    };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n\n        /** @type {typeof MapModel} */\n        const Model = this.props.Model;\n        const model = useModelWithSampleData(Model, this.props.modelParams);\n        this.model = model;\n\n        onWillUnmount(() => {\n            this.model.stopFetchingCoordinates();\n        });\n\n        useSetupAction({\n            getLocalState: () => {\n                return this.model.metaData;\n            },\n        });\n\n        onWillStart(() =>\n            Promise.all([\n                loadJS(\"/web_map/static/lib/leaflet/leaflet.js\"),\n                loadCSS(\"/web_map/static/lib/leaflet/leaflet.css\"),\n            ])\n        );\n\n        usePager(() => {\n            return {\n                offset: this.model.metaData.offset,\n                limit: this.model.metaData.limit,\n                total: this.model.data.count,\n                onUpdate: ({ offset, limit }) => this.model.load({ offset, limit }),\n            };\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    /**\n     * @returns {any}\n     */\n    get rendererProps() {\n        return {\n            model: this.model,\n            onMarkerClick: this.openRecords.bind(this),\n        };\n    }\n\n    /**\n     * Redirects to views when clicked on open button in marker popup.\n     *\n     * @param {number[]} ids\n     */\n    openRecords(ids) {\n        if (ids.length > 1) {\n            this.action.doAction({\n                type: \"ir.actions.act_window\",\n                name: this.env.config.getDisplayName() || _t(\"Untitled\"),\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                res_model: this.props.resModel,\n                domain: [[\"id\", \"in\", ids]],\n            });\n        } else {\n            this.action.switchView(\"form\", { resId: ids[0] });\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Model } from \"@web/model/model\";\nimport { session } from \"@web/session\";\nimport { resequence } from \"@web/model/relational_model/utils\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { formatDateTime, parseDate, parseDateTime } from \"@web/core/l10n/dates\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\nconst DATE_GROUP_FORMATS = {\n    year: \"yyyy\",\n    quarter: \"'Q'q yyyy\",\n    month: \"MMMM yyyy\",\n    week: \"'W'WW yyyy\",\n    day: \"dd MMM yyyy\",\n};\n\nexport class MapModel extends Model {\n    setup(params, { notification, http }) {\n        this.notification = notification;\n        this.http = http;\n\n        this.metaData = {\n            ...params,\n            mapBoxToken: session.map_box_token || \"\",\n        };\n\n        this.data = {\n            count: 0,\n            fetchingCoordinates: false,\n            groupByKey: false,\n            isGrouped: false,\n            numberOfLocatedRecords: 0,\n            partners: {},\n            partnerToCache: [],\n            recordGroups: [],\n            records: [],\n            routes: [],\n            routingError: null,\n            shouldUpdatePosition: true,\n            useMapBoxAPI: !!this.metaData.mapBoxToken,\n        };\n\n        this.coordinateFetchingTimeoutHandle = undefined;\n        this.shouldFetchCoordinates = false;\n        this.keepLast = new KeepLast();\n    }\n    /**\n     * @param {any} params\n     * @returns {Promise<void>}\n     */\n    async load(params) {\n        if (this.coordinateFetchingTimeoutHandle !== undefined) {\n            this.stopFetchingCoordinates();\n        }\n        const metaData = {\n            ...this.metaData,\n            ...params,\n        };\n\n        // remove the properties fields from the group by\n        metaData.groupBy = (metaData.groupBy || []).filter((groupBy) => {\n            // properties fields are in the form `[propert_field_name].[property_entry_key]`\n            const [fieldName] = groupBy.split(\".\");\n            const field = metaData.fields[fieldName];\n            return field?.type !== \"properties\";\n        });\n\n        this.data = await this._fetchData(metaData);\n        this.metaData = metaData;\n\n        this.notify();\n    }\n    /**\n     * Tells the model to stop fetching coordinates.\n     * In OSM mode, the model starts to fetch coordinates once every second after the\n     * model has loaded.\n     * This fetching has to be done every second if we don't want to be banned from OSM.\n     * There are typically two cases when we need to stop fetching:\n     * - when component is about to be unmounted because the request is bound to\n     *   the component and it will crash if we do so.\n     * - when calling the `load` method as it will start fetching new coordinates.\n     */\n    stopFetchingCoordinates() {\n        browser.clearTimeout(this.coordinateFetchingTimeoutHandle);\n        this.coordinateFetchingTimeoutHandle = undefined;\n        this.shouldFetchCoordinates = false;\n    }\n\n    get canResequence() {\n        return (\n            this.metaData.defaultOrder &&\n            !this.metaData.fields[this.metaData.defaultOrder.name].readonly &&\n            this.metaData.fields[this.metaData.defaultOrder.name].type === \"integer\" &&\n            this.metaData.allowResequence &&\n            !this.metaData.groupBy?.length\n        );\n    }\n\n    /**\n     * Resequence the records in `this.data.records` such that the record with the id\n     * `movedRecordId` is moved after the record with the id `targetRecordId`\n     * @param {Number} movedRecordId\n     * @param {Number} targetRecordId\n     */\n    async resequence(movedId, targetId) {\n        const fieldName = this.metaData.defaultOrder.name;\n        const asc = this.metaData.defaultOrder.asc;\n        const resequenceProm = resequence({\n            records: this.data.records,\n            resModel: this.metaData.resModel,\n            movedId,\n            targetId,\n            fieldName,\n            asc,\n            context: this.metaData.context,\n            orm: this.orm,\n        });\n        // the resequence method modifies this.data.records before the resequence backend call\n        // we need to notify after the synchronous record change\n        this.notify();\n        const resequencedRecords = await resequenceProm;\n        if (resequencedRecords) {\n            for (const resequencedRecord of resequencedRecords) {\n                const record = this.data.records.find((r) => r.id === resequencedRecord.id);\n                record[fieldName] = resequencedRecord[fieldName];\n            }\n            await this._updatePartnerCoordinate(this.metaData, this.data);\n            this.notify();\n        }\n    }\n\n    //----------------------------------------------------------------------\n    // Protected\n    //----------------------------------------------------------------------\n\n    /**\n     * Adds the corresponding partner to a record.\n     *\n     * @protected\n     */\n    _addPartnerToRecord(metaData, data) {\n        for (const record of data.records) {\n            if (metaData.resModel === \"res.partner\" && metaData.resPartnerField === \"id\") {\n                record.partner = data.partners[record.id];\n            } else {\n                record.partner = data.partners[record[metaData.resPartnerField].id];\n            }\n            data.numberOfLocatedRecords++;\n        }\n    }\n\n    /**\n     * The partner's coordinates should be between -90 <= latitude <= 90 and -180 <= longitude <= 180.\n     *\n     * @protected\n     * @param {Object} partner\n     * @param {number} partner.partner_latitude latitude of the partner\n     * @param {number} partner.partner_longitude longitude of the partner\n     * @returns {boolean}\n     */\n    _checkCoordinatesValidity(partner) {\n        if (\n            partner.partner_latitude &&\n            partner.partner_longitude &&\n            partner.partner_latitude >= -90 &&\n            partner.partner_latitude <= 90 &&\n            partner.partner_longitude >= -180 &&\n            partner.partner_longitude <= 180\n        ) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Handles the case of an empty map.\n     * Handles the case where the model is res_partner.\n     * Fetches the records according to the model given in the arch.\n     * If the records has no partner_id field it is sliced from the array.\n     *\n     * @protected\n     * @params {any} metaData\n     * @return {Promise<any>}\n     */\n    async _fetchData(metaData) {\n        const data = {\n            count: 0,\n            fetchingCoordinates: false,\n            groupByKey: metaData.groupBy.length ? metaData.groupBy[0] : false,\n            isGrouped: metaData.groupBy.length > 0,\n            numberOfLocatedRecords: 0,\n            partners: {},\n            partnerToCache: [],\n            recordGroups: [],\n            records: [],\n            routes: [],\n            routingError: null,\n            shouldUpdatePosition: true,\n            useMapBoxAPI: !!metaData.mapBoxToken,\n        };\n\n        //case of empty map\n        if (!metaData.resPartnerField) {\n            data.recordGroups = [];\n            data.records = [];\n            data.routes = [];\n            return this.keepLast.add(Promise.resolve(data));\n        }\n        const results = await this.keepLast.add(this._fetchRecordData(metaData, data));\n\n        const datetimeFields = metaData.fieldNames.filter(\n            (name) => metaData.fields[name].type == \"datetime\"\n        );\n        for (const record of results.records) {\n            // convert date fields from UTC to local timezone\n            for (const field of datetimeFields) {\n                if (record[field]) {\n                    const dateUTC = luxon.DateTime.fromFormat(\n                        record[field],\n                        \"yyyy-MM-dd HH:mm:ss\",\n                        { zone: \"UTC\" }\n                    );\n                    record[field] = formatDateTime(dateUTC, { format: \"yyyy-MM-dd HH:mm:ss\" });\n                }\n            }\n        }\n\n        data.records = results.records;\n        data.count = results.length;\n        if (data.isGrouped) {\n            data.recordGroups = await this._getRecordGroups(metaData, data);\n        } else {\n            data.recordGroups = [];\n        }\n\n        if (metaData.resModel === \"res.partner\" && metaData.resPartnerField === \"id\") {\n            for (const record of data.records) {\n                if (!data.partners[record.id]) {\n                    data.partners[record.id] = { ...record };\n                }\n            }\n        } else {\n            for (const record of data.records) {\n                const partner = record[metaData.resPartnerField];\n                if (partner && !data.partners[partner.id]) {\n                    data.partners[partner.id] = partner;\n                }\n            }\n        }\n        this._addPartnerToRecord(metaData, data);\n        await this._updatePartnerCoordinate(metaData, data);\n        return data;\n    }\n\n    _getRecordSpecification(metaData, data) {\n        const fieldNames = data.groupByKey\n            ? metaData.fieldNames.concat(data.groupByKey.split(\":\")[0])\n            : metaData.fieldNames;\n        const specification = {};\n        const fieldsToAdd = {\n            contact_address_complete: {},\n            partner_latitude: {},\n            partner_longitude: {},\n        };\n        for (const fieldName of fieldNames) {\n            specification[fieldName] = {};\n            if (fieldName === \"id\" && metaData.resPartnerField === \"id\") {\n                Object.assign(specification, fieldsToAdd);\n            } else if (\n                [\"many2one\", \"one2many\", \"many2many\"].includes(metaData.fields[fieldName].type)\n            ) {\n                specification[fieldName].fields = { display_name: {} };\n                if (fieldName === metaData.resPartnerField) {\n                    Object.assign(specification[fieldName].fields, fieldsToAdd);\n                }\n            }\n        }\n        return specification;\n    }\n\n    /**\n     * Fetch the records for a given model.\n     *\n     * @protected\n     * @returns {Promise}\n     */\n    _fetchRecordData(metaData, data) {\n        const specification = this._getRecordSpecification(metaData, data);\n        const orderBy = [];\n        if (metaData.defaultOrder) {\n            orderBy.push(metaData.defaultOrder.name);\n            if (metaData.defaultOrder.asc) {\n                orderBy.push(\"ASC\");\n            }\n        }\n        return this.orm.webSearchRead(metaData.resModel, metaData.domain, {\n            specification,\n            limit: metaData.limit,\n            offset: metaData.offset,\n            order: orderBy.join(\" \"),\n            context: metaData.context,\n        });\n    }\n\n    /**\n     * This function convert the addresses to coordinates using the mapbox API.\n     *\n     * @protected\n     * @param {Object} record this object contains the record fetched from the database.\n     * @returns {Promise} result.query contains the query the the api received\n     *      result.features contains results in descendant order of relevance\n     */\n    _fetchCoordinatesFromAddressMB(metaData, data, record) {\n        const address = encodeURIComponent(record.contact_address_complete);\n        const token = metaData.mapBoxToken;\n        const encodedUrl = `https://api.mapbox.com/geocoding/v5/mapbox.places/${address}.json?access_token=${token}&cachebuster=1552314159970&autocomplete=true`;\n        return this.http.get(encodedUrl);\n    }\n\n    /**\n     * This function convert the addresses to coordinates using the openStreetMap api.\n     *\n     * @protected\n     * @param {Object} record this object contains the record fetched from the database.\n     * @returns {Promise} result is an array that contains the result in descendant order of relevance\n     *      result[i].lat is the latitude of the converted address\n     *      result[i].lon is the longitude of the converted address\n     *      result[i].importance is a number that the relevance of the result the closer the number is to one the best it is.\n     */\n    _fetchCoordinatesFromAddressOSM(metaData, data, record) {\n        const address = encodeURIComponent(record.contact_address_complete.replace(\"/\", \" \"));\n        const encodedUrl = `https://nominatim.openstreetmap.org/search?q=${address}&format=jsonv2`;\n        return this.http.get(encodedUrl);\n    }\n\n    /**\n     * Fetch the route from the mapbox api.\n     *\n     * @protected\n     * @returns {Promise}\n     *      results.geometry.legs[i] contains one leg (i.e: the trip between two markers).\n     *      results.geometry.legs[i].steps contains the sets of coordinates to follow to reach a point from an other.\n     *      results.geometry.legs[i].distance: the distance in meters to reach the destination\n     *      results.geometry.legs[i].duration the duration of the leg\n     *      results.geometry.coordinates contains the sets of coordinates to go from the first to the last marker without the notion of waypoint\n     */\n    _fetchRoute(metaData, data) {\n        const coordinatesParam = data.records\n            .filter(\n                (record) =>\n                    record.partner &&\n                    record.partner.partner_latitude &&\n                    record.partner.partner_longitude\n            )\n            .map(({ partner }) => `${partner.partner_longitude},${partner.partner_latitude}`);\n        const address = encodeURIComponent(coordinatesParam.join(\";\"));\n        const token = metaData.mapBoxToken;\n        const encodedUrl = `https://api.mapbox.com/directions/v5/mapbox/driving/${address}?access_token=${token}&steps=true&geometries=geojson`;\n        return this.http.get(encodedUrl);\n    }\n\n    /**\n     * Converts a MapBox error message into a custom translatable one.\n     *\n     * @protected\n     * @param {string} message\n     */\n    _getErrorMessage(message) {\n        const ERROR_MESSAGES = {\n            \"Too many coordinates; maximum number of coordinates is 25\": _t(\n                \"Too many routing points (maximum 25)\"\n            ),\n            \"Route exceeds maximum distance limitation\": _t(\n                \"Some routing points are too far apart\"\n            ),\n            \"Too Many Requests\": _t(\"Too many requests, try again in a few minutes\"),\n        };\n        return ERROR_MESSAGES[message];\n    }\n\n    _getEmptyGroupLabel(fieldName) {\n        return _t(\"None\");\n    }\n\n    /**\n     * @protected\n     * @returns {Object} the fetched records grouped by the groupBy field.\n     */\n    async _getRecordGroups(metaData, data) {\n        const [fieldName, subGroup] = data.groupByKey.split(\":\");\n        const fieldType = metaData.fields[fieldName].type;\n        const groups = {};\n        function addToGroup(id, name, record) {\n            if (!groups[id]) {\n                groups[id] = {\n                    name,\n                    records: [],\n                };\n            }\n            groups[id].records.push(record);\n        }\n        for (const record of data.records) {\n            const value = record[fieldName];\n            let id, name;\n            if ([\"one2many\", \"many2many\"].includes(fieldType)) {\n                if (value.length) {\n                    for (const r of value) {\n                        addToGroup(r.id, r.display_name, record);\n                    }\n                } else {\n                    id = name = this._getEmptyGroupLabel(fieldName);\n                    addToGroup(id, name, record);\n                }\n            } else {\n                if ([\"date\", \"datetime\"].includes(fieldType) && value) {\n                    const date = fieldType === \"date\" ? parseDate(value) : parseDateTime(value);\n                    id = name = date.toFormat(DATE_GROUP_FORMATS[subGroup]);\n                } else if (fieldType === \"boolean\") {\n                    id = name = value ? _t(\"Yes\") : _t(\"No\");\n                } else if (fieldType === \"selection\") {\n                    const selected = metaData.fields[fieldName].selection.find(\n                        (o) => o[0] === value\n                    );\n                    id = name = selected ? selected[1] : value;\n                } else if (fieldType === \"many2one\" && value) {\n                    id = value.id;\n                    name = value.display_name;\n                } else {\n                    id = value;\n                    name = value;\n                }\n                if (!id && !name) {\n                    id = name = this._getEmptyGroupLabel(fieldName);\n                }\n                addToGroup(id, name, record);\n            }\n        }\n        return groups;\n    }\n\n    /**\n     * Handles the case where the selected api is MapBox.\n     * Iterates on all the partners and fetches their coordinates when they're not set.\n     *\n     * @protected\n     * @return {Promise} if there's more than 2 located records and the routing option is activated it returns a promise that fetches the route\n     *      resultResult is an object that contains the computed route\n     *      or if either of these conditions are not respected it returns an empty promise\n     */\n    _maxBoxAPI(metaData, data) {\n        const promises = [];\n        for (const partner of Object.values(data.partners)) {\n            if (\n                partner.contact_address_complete &&\n                (!partner.partner_latitude || !partner.partner_longitude)\n            ) {\n                promises.push(\n                    this._fetchCoordinatesFromAddressMB(metaData, data, partner).then(\n                        (coordinates) => {\n                            if (coordinates.features.length) {\n                                partner.partner_longitude = parseFloat(\n                                    coordinates.features[0].geometry.coordinates[0]\n                                );\n                                partner.partner_latitude = parseFloat(\n                                    coordinates.features[0].geometry.coordinates[1]\n                                );\n                                data.partnerToCache.push(partner);\n                            }\n                        }\n                    )\n                );\n            } else if (!this._checkCoordinatesValidity(partner)) {\n                partner.partner_latitude = undefined;\n                partner.partner_longitude = undefined;\n            }\n        }\n        return Promise.all(promises).then(() => {\n            data.routes = [];\n            if (data.numberOfLocatedRecords > 1 && metaData.routing && !data.groupByKey) {\n                return this._fetchRoute(metaData, data).then((routeResult) => {\n                    if (routeResult.routes) {\n                        data.routes = routeResult.routes;\n                    } else {\n                        data.routingError = this._getErrorMessage(routeResult.message);\n                    }\n                });\n            } else {\n                return Promise.resolve();\n            }\n        });\n    }\n\n    /**\n     * Handles the displaying of error message according to the error.\n     *\n     * @protected\n     * @param {Object} err contains the error returned by the requests\n     * @param {number} err.status contains the status_code of the failed http request\n     */\n    _mapBoxErrorHandling(metaData, data, err) {\n        switch (err.status) {\n            case 401:\n                this.notification.add(\n                    _t(\n                        \"The view has switched to another provider but functionalities will be limited\"\n                    ),\n                    {\n                        title: _t(\"Token invalid\"),\n                        type: \"danger\",\n                    }\n                );\n                break;\n            case 403:\n                this.notification.add(\n                    _t(\n                        \"The view has switched to another provider but functionalities will be limited\"\n                    ),\n                    {\n                        title: _t(\"Unauthorized connection\"),\n                        type: \"danger\",\n                    }\n                );\n                break;\n            case 422: // Max. addresses reached\n            case 429: // Max. requests reached\n                data.routingError = this._getErrorMessage(err.responseJSON.message);\n                break;\n            case 500:\n                this.notification.add(\n                    _t(\n                        \"The view has switched to another provider but functionalities will be limited\"\n                    ),\n                    {\n                        title: _t(\"MapBox servers unreachable\"),\n                        type: \"danger\",\n                    }\n                );\n        }\n    }\n\n    /**\n     * Notifies the fetched coordinates to server and controller.\n     *\n     * @protected\n     */\n    _notifyFetchedCoordinate(metaData, data) {\n        this._writeCoordinatesUsers(metaData, data);\n        data.shouldUpdatePosition = false;\n        this.notify();\n    }\n\n    /**\n     * Calls (without awaiting) _openStreetMapAPIAsync with a delay of 1000ms\n     * to not get banned from openstreetmap's server.\n     *\n     * Tests should patch this function to wait for coords to be fetched.\n     *\n     * @see _openStreetMapAPIAsync\n     * @protected\n     * @return {Promise}\n     */\n    _openStreetMapAPI(metaData, data) {\n        this._openStreetMapAPIAsync(metaData, data);\n        return Promise.resolve();\n    }\n    /**\n     * Handles the case where the selected api is open street map.\n     * Iterates on all the partners and fetches their coordinates when they're not set.\n     *\n     * @protected\n     * @returns {Promise}\n     */\n    _openStreetMapAPIAsync(metaData, data) {\n        // Group partners by address to reduce address list\n        const addressPartnerMap = new Map();\n        for (const partner of Object.values(data.partners)) {\n            if (\n                partner.contact_address_complete &&\n                (!partner.partner_latitude || !partner.partner_longitude)\n            ) {\n                if (!addressPartnerMap.has(partner.contact_address_complete)) {\n                    addressPartnerMap.set(partner.contact_address_complete, []);\n                }\n                addressPartnerMap.get(partner.contact_address_complete).push(partner);\n                partner.fetchingCoordinate = true;\n            } else if (!this._checkCoordinatesValidity(partner)) {\n                partner.partner_latitude = undefined;\n                partner.partner_longitude = undefined;\n            }\n        }\n\n        // `fetchingCoordinates` is used to display the \"fetching banner\"\n        // We need to check if there are coordinates to fetch before reload the\n        // view to prevent flickering\n        data.fetchingCoordinates = addressPartnerMap.size > 0;\n        this.shouldFetchCoordinates = true;\n        const fetch = async () => {\n            const partnersList = Array.from(addressPartnerMap.values());\n            for (let i = 0; i < partnersList.length; i++) {\n                await new Promise((resolve) => {\n                    this.coordinateFetchingTimeoutHandle = browser.setTimeout(\n                        resolve,\n                        this.constructor.COORDINATE_FETCH_DELAY\n                    );\n                });\n                if (!this.shouldFetchCoordinates) {\n                    return;\n                }\n                const partners = partnersList[i];\n                try {\n                    const coordinates = await this._fetchCoordinatesFromAddressOSM(\n                        metaData,\n                        data,\n                        partners[0]\n                    );\n                    if (!this.shouldFetchCoordinates) {\n                        return;\n                    }\n                    if (coordinates.length) {\n                        for (const partner of partners) {\n                            partner.partner_longitude = parseFloat(coordinates[0].lon);\n                            partner.partner_latitude = parseFloat(coordinates[0].lat);\n                            data.partnerToCache.push(partner);\n                        }\n                    }\n                    for (const partner of partners) {\n                        partner.fetchingCoordinate = false;\n                    }\n                    data.fetchingCoordinates = i < partnersList.length - 1;\n                    this._notifyFetchedCoordinate(metaData, data);\n                } catch {\n                    for (const partner of Object.values(data.partners)) {\n                        partner.fetchingCoordinate = false;\n                    }\n                    data.fetchingCoordinates = false;\n                    this.shouldFetchCoordinates = false;\n                    this.notification.add(\n                        _t(\"OpenStreetMap's request limit exceeded, try again later.\"),\n                        { type: \"danger\" }\n                    );\n                    this.notify();\n                }\n            }\n        };\n        return fetch();\n    }\n\n    /**\n     * if the token is set it uses the mapBoxApi to fetch address and route\n     * if not is uses the openstreetmap api to fetch the address.\n     *\n     * @protected\n     * @returns {Promise}\n     */\n    async _updatePartnerCoordinate(metaData, data) {\n        if (data.useMapBoxAPI) {\n            return this.keepLast\n                .add(this._maxBoxAPI(metaData, data))\n                .then(() => {\n                    this._writeCoordinatesUsers(metaData, data);\n                })\n                .catch((err) => {\n                    this._mapBoxErrorHandling(metaData, data, err);\n                    data.useMapBoxAPI = false;\n                    return this._openStreetMapAPI(metaData, data);\n                });\n        } else {\n            return this._openStreetMapAPI(metaData, data).then(() => {\n                this._writeCoordinatesUsers(metaData, data);\n            });\n        }\n    }\n    /**\n     * Writes partner_longitude and partner_latitude of the res.partner model.\n     *\n     * @protected\n     * @return {Promise}\n     */\n    async _writeCoordinatesUsers(metaData, data) {\n        const partners = data.partnerToCache;\n        data.partnerToCache = [];\n        if (partners.length) {\n            await this.orm.call(\"res.partner\", \"update_latitude_longitude\", [partners], {\n                context: metaData.context,\n            });\n        }\n    }\n}\n\nMapModel.services = [\"notification\", \"http\"];\nMapModel.COORDINATE_FETCH_DELAY = 1000;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\n/*global L*/\n\nimport { renderToString } from \"@web/core/utils/render\";\nimport { delay } from \"@web/core/utils/concurrency\";\n\nimport {\n    Component,\n    onWillUnmount,\n    onWillUpdateProps,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nconst apiTilesRouteWithToken =\n    \"https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}\";\nconst apiTilesRouteWithoutToken = \"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png\";\n\nconst colors = [\n    \"#F06050\",\n    \"#6CC1ED\",\n    \"#F7CD1F\",\n    \"#814968\",\n    \"#30C381\",\n    \"#D6145F\",\n    \"#475577\",\n    \"#F4A460\",\n    \"#EB7E7F\",\n    \"#2C8397\",\n];\n\nconst mapTileAttribution = `\n    \u00a9 <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a>\n    \u00a9 <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>\n    <strong>\n        <a href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">\n            Improve this map\n        </a>\n    </strong>`;\n\nexport class MapRenderer extends Component {\n    static template = \"web_map.MapRenderer\";\n    static props = {\n        model: Object,\n        onMarkerClick: Function,\n    };\n\n    setup() {\n        this.leafletMap = null;\n        this.markers = [];\n        this.polylines = [];\n        this.mapContainerRef = useRef(\"mapContainer\");\n        this.state = useState({\n            closedGroupIds: [],\n            expendedPinList: false,\n        });\n        this.nextId = 1;\n\n        useEffect(\n            () => {\n                this.leafletMap = L.map(this.mapContainerRef.el, {\n                    maxBounds: [L.latLng(180, -180), L.latLng(-180, 180)],\n                });\n                this.leafletMap.attributionControl.setPrefix(\n                    '<a href=\"https://leafletjs.com\" title=\"A JavaScript library for interactive maps\">Leaflet</a>'\n                );\n                L.tileLayer(this.apiTilesRoute, {\n                    attribution: mapTileAttribution,\n                    tileSize: 512,\n                    zoomOffset: -1,\n                    minZoom: 2,\n                    maxZoom: 19,\n                    id: \"mapbox/streets-v11\",\n                    accessToken: this.props.model.metaData.mapBoxToken,\n                }).addTo(this.leafletMap);\n            },\n            () => []\n        );\n        useEffect(() => {\n            this.updateMap();\n        });\n\n        this.pinListRef = useRef(\"pinList\");\n        useSortable({\n            enable: () => this.props.model.canResequence,\n            ref: this.pinListRef,\n            elements: \".o-map-renderer--pin-located\",\n            handle: \".o_row_handle\",\n            onDrop: async (params) => {\n                const rowId = parseInt(params.element.dataset.id);\n                const previousRowId = parseInt(params.previous?.dataset?.id) || null;\n                await this.props.model.resequence(rowId, previousRowId);\n            },\n        });\n\n        onWillUpdateProps(this.onWillUpdateProps);\n        onWillUnmount(this.onWillUnmount);\n    }\n    /**\n     * Update group opened/closed state.\n     */\n    async onWillUpdateProps(nextProps) {\n        if (this.props.model.data.groupByKey !== nextProps.model.data.groupByKey) {\n            this.state.closedGroupIds = [];\n        }\n    }\n    /**\n     * Remove map and the listeners on its markers and routes.\n     */\n    onWillUnmount() {\n        this.removeMarkers();\n        this.removeRoutes();\n        if (this.leafletMap) {\n            this.leafletMap.remove();\n        }\n    }\n\n    /**\n     * Return the route to the tiles api with or without access token.\n     *\n     * @returns {string}\n     */\n    get apiTilesRoute() {\n        return this.props.model.data.useMapBoxAPI\n            ? apiTilesRouteWithToken\n            : apiTilesRouteWithoutToken;\n    }\n\n    /**\n     * If there's located records, adds the corresponding marker on the map.\n     * Binds events to the created markers.\n     */\n    addMarkers() {\n        this.removeMarkers();\n\n        const markersInfo = {};\n        let records = this.props.model.data.records;\n        if (this.props.model.data.isGrouped) {\n            records = Object.entries(this.props.model.data.recordGroups)\n                .filter(([key]) => !this.state.closedGroupIds.includes(key))\n                .flatMap(([groupId, value]) => value.records.map((elem) => ({ ...elem, groupId })));\n        }\n\n        const pinInSamePlace = {};\n        for (const record of records) {\n            const partner = record.partner;\n            if (partner && partner.partner_latitude && partner.partner_longitude) {\n                const lat_long = `${partner.partner_latitude}-${partner.partner_longitude}`;\n                const group = this.props.model.data.recordGroups ? `-${record.groupId}` : \"\";\n                const key = `${lat_long}${group}`;\n                if (key in markersInfo) {\n                    markersInfo[key].record = record;\n                    markersInfo[key].ids.push(record.id);\n                } else {\n                    pinInSamePlace[lat_long] = ++pinInSamePlace[lat_long] || 0;\n                    markersInfo[key] = {\n                        record: record,\n                        ids: [record.id],\n                        pinInSamePlace: pinInSamePlace[lat_long],\n                    };\n                }\n            }\n        }\n\n        for (const markerInfo of Object.values(markersInfo)) {\n            const params = {\n                count: markerInfo.ids.length,\n                isMulti: markerInfo.ids.length > 1,\n                number: this.props.model.data.records.indexOf(markerInfo.record) + 1,\n                numbering: this.props.model.metaData.numbering,\n            };\n\n            if (this.props.model.data.isGrouped) {\n                const groupId = markerInfo.record.groupId;\n                params.color = this.getGroupColor(groupId);\n                params.number =\n                    this.props.model.data.recordGroups[groupId].records.findIndex((record) => {\n                        return record.id === markerInfo.record.id;\n                    }) + 1;\n            }\n\n            // Icon creation\n            const iconInfo = {\n                className: \"o-map-renderer--marker\",\n                html: renderToString(\"web_map.marker\", params),\n            };\n\n            const offset = markerInfo.pinInSamePlace * 0.000025;\n            // Attach marker with icon and popup\n            const marker = L.marker(\n                [\n                    markerInfo.record.partner.partner_latitude + offset,\n                    markerInfo.record.partner.partner_longitude - offset,\n                ],\n                { icon: L.divIcon(iconInfo) }\n            );\n            marker.addTo(this.leafletMap);\n            marker.on(\"click\", () => {\n                this.createMarkerPopup(markerInfo, offset);\n            });\n            this.markers.push(marker);\n        }\n    }\n    /**\n     * If there are computed routes, create polylines and add them to the map.\n     * each element of this.props.routeInfo[0].legs array represent the route between\n     * two waypoints thus each of these must be a polyline.\n     */\n    addRoutes() {\n        this.removeRoutes();\n        if (!this.props.model.data.useMapBoxAPI || !this.props.model.data.routes.length) {\n            return;\n        }\n\n        for (const leg of this.props.model.data.routes[0].legs) {\n            const latLngs = [];\n            for (const step of leg.steps) {\n                for (const coordinate of step.geometry.coordinates) {\n                    latLngs.push(L.latLng(coordinate[1], coordinate[0]));\n                }\n            }\n\n            const polyline = L.polyline(latLngs, {\n                color: \"blue\",\n                weight: 5,\n                opacity: 0.3,\n            }).addTo(this.leafletMap);\n\n            const polylines = this.polylines;\n            polyline.on(\"click\", function () {\n                for (const polyline of polylines) {\n                    polyline.setStyle({ color: \"blue\", opacity: 0.3 });\n                }\n                this.setStyle({ color: \"darkblue\", opacity: 1.0 });\n            });\n            this.polylines.push(polyline);\n        }\n    }\n    /**\n     * Create a popup for the specified marker.\n     *\n     * @param {Object} markerInfo\n     * @param {Number} latLongOffset\n     */\n    createMarkerPopup(markerInfo, latLongOffset = 0) {\n        const popupFields = this.getMarkerPopupFields(markerInfo);\n        const partner = markerInfo.record.partner;\n        const encodedAddress = encodeURIComponent(partner.contact_address_complete);\n        const popupHtml = renderToString(\"web_map.markerPopup\", {\n            fields: popupFields,\n            hasFormView: this.props.model.metaData.hasFormView,\n            url: `https://www.google.com/maps/dir/?api=1&destination=${encodedAddress}`,\n        });\n\n        const popup = L.popup({ offset: [0, -30] })\n            .setLatLng([\n                partner.partner_latitude + latLongOffset,\n                partner.partner_longitude - latLongOffset,\n            ])\n            .setContent(popupHtml)\n            .openOn(this.leafletMap);\n\n        const openBtn = popup\n            .getElement()\n            .querySelector(\"button.o-map-renderer--popup-buttons-open\");\n        if (openBtn) {\n            openBtn.onclick = () => {\n                this.props.onMarkerClick(markerInfo.ids);\n            };\n        }\n        return popup;\n    }\n    /**\n     * @param {Number} groupId\n     */\n    getGroupColor(groupId) {\n        const index = Object.keys(this.props.model.data.recordGroups).indexOf(groupId);\n        return colors[index % colors.length];\n    }\n    /**\n     * Creates an array of latLng objects if there is located records.\n     *\n     * @returns {latLngBounds|boolean} objects containing the coordinates that\n     *          allows all the records to be shown on the map or returns false\n     *          if the records does not contain any located record.\n     */\n    getLatLng() {\n        const tabLatLng = [];\n        for (const record of this.props.model.data.records) {\n            const partner = record.partner;\n            if (partner && partner.partner_latitude && partner.partner_longitude) {\n                tabLatLng.push(L.latLng(partner.partner_latitude, partner.partner_longitude));\n            }\n        }\n        if (!tabLatLng.length) {\n            return false;\n        }\n        return L.latLngBounds(tabLatLng);\n    }\n    /**\n     * Get the fields' name and value to display in the popup.\n     *\n     * @param {Object} markerInfo\n     * @returns {Object} value contains the value of the field and string\n     *                   contains the value of the xml's string attribute\n     */\n    getMarkerPopupFields(markerInfo) {\n        const record = markerInfo.record;\n        const fieldsView = [];\n        // Only display address in multi coordinates marker popup\n        if (markerInfo.ids.length > 1) {\n            if (!this.props.model.metaData.hideAddress) {\n                fieldsView.push({\n                    id: this.nextId++,\n                    value: record.partner.contact_address_complete,\n                    string: _t(\"Address\"),\n                });\n            }\n            return fieldsView;\n        }\n        if (!this.props.model.metaData.hideName) {\n            fieldsView.push({\n                id: this.nextId++,\n                value: record.display_name,\n                string: _t(\"Name\"),\n            });\n        }\n        if (!this.props.model.metaData.hideAddress) {\n            fieldsView.push({\n                id: this.nextId++,\n                value: record.partner.contact_address_complete,\n                string: _t(\"Address\"),\n            });\n        }\n        const fields = this.props.model.metaData.fields;\n        for (const field of this.props.model.metaData.fieldNamesMarkerPopup) {\n            if (record[field.fieldName]) {\n                let value = record[field.fieldName];\n                if (fields[field.fieldName].type === \"many2one\") {\n                    value = record[field.fieldName].display_name;\n                } else if ([\"one2many\", \"many2many\"].includes(fields[field.fieldName].type)) {\n                    value = record[field.fieldName]\n                        ? record[field.fieldName].map((r) => r.display_name).join(\", \")\n                        : \"\";\n                }\n                fieldsView.push({\n                    id: this.nextId++,\n                    value,\n                    string: field.string,\n                });\n            }\n        }\n        return fieldsView;\n    }\n    /**\n     * @returns {string}\n     */\n    get googleMapUrl() {\n        let url = \"https://www.google.com/maps/dir/?api=1\";\n        if (this.props.model.data.records.length) {\n            const allCoordinates = this.props.model.data.records.filter(\n                ({ partner }) => partner && partner.partner_latitude && partner.partner_longitude\n            );\n            const uniqueCoordinates = allCoordinates.reduce((coords, { partner }) => {\n                const coord = partner.partner_latitude + \",\" + partner.partner_longitude;\n                if (!coords.includes(coord)) {\n                    coords.push(coord);\n                }\n                return coords;\n            }, []);\n            if (uniqueCoordinates.length && this.props.model.metaData.routing) {\n                // When routing is enabled, make last record the destination\n                url += `&destination=${uniqueCoordinates.pop()}`;\n            }\n            if (uniqueCoordinates.length) {\n                url += `&waypoints=${uniqueCoordinates.join(\"|\")}`;\n            }\n        }\n        return url;\n    }\n    /**\n     * Remove the markers from the map and empty the markers array.\n     */\n    removeMarkers() {\n        for (const marker of this.markers) {\n            marker.off(\"click\");\n            this.leafletMap.removeLayer(marker);\n        }\n        this.markers = [];\n    }\n    /**\n     * Remove the routes from the map and empty the the polyline array.\n     */\n    removeRoutes() {\n        for (const polyline of this.polylines) {\n            polyline.off(\"click\");\n            this.leafletMap.removeLayer(polyline);\n        }\n        this.polylines = [];\n    }\n    /**\n     * Update position in the map, markers and routes.\n     */\n    updateMap() {\n        if (this.props.model.data.shouldUpdatePosition) {\n            const initialCoord = this.getLatLng();\n            if (initialCoord) {\n                this.leafletMap.flyToBounds(initialCoord, { animate: false });\n            } else {\n                this.leafletMap.fitWorld();\n            }\n            this.leafletMap.closePopup();\n        }\n        this.addMarkers();\n        this.addRoutes();\n    }\n\n    /**\n     * Center the map on a certain pin and open the popup linked to it.\n     *\n     * @param {Object} record\n     */\n    async centerAndOpenPin(record) {\n        this.state.expendedPinList = false;\n        // wait the next owl render to avoid marker popup create => destroy\n        await delay(0);\n        const popup = this.createMarkerPopup({\n            record: record,\n            ids: [record.id],\n        });\n        const px = this.leafletMap.project([\n            record.partner.partner_latitude,\n            record.partner.partner_longitude,\n        ]);\n        const popupHeight = popup.getElement().offsetHeight;\n        px.y -= popupHeight / 2;\n        const latlng = this.leafletMap.unproject(px);\n        this.leafletMap.panTo(latlng, { animate: true });\n    }\n    /**\n     * @param {Number} id\n     */\n    toggleGroup(id) {\n        if (this.state.closedGroupIds.includes(id)) {\n            const index = this.state.closedGroupIds.indexOf(id);\n            this.state.closedGroupIds.splice(index, 1);\n        } else {\n            this.state.closedGroupIds.push(id);\n        }\n    }\n\n    togglePinList() {\n        this.state.expendedPinList = !this.state.expendedPinList;\n    }\n\n    get expendedPinList() {\n        return this.env.isSmall ? this.state.expendedPinList : false;\n    }\n\n    get canDisplayPinList() {\n        return !this.env.isSmall || this.expendedPinList;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { MapArchParser } from \"./map_arch_parser\";\nimport { MapModel } from \"./map_model\";\nimport { MapController } from \"./map_controller\";\nimport { MapRenderer } from \"./map_renderer\";\n\nexport const mapView = {\n    type: \"map\",\n    Controller: MapController,\n    Renderer: MapRenderer,\n    Model: MapModel,\n    ArchParser: MapArchParser,\n    buttonTemplate: \"web_map.MapView.Buttons\",\n\n    props: (genericProps, view, config) => {\n        let modelParams = genericProps.state;\n        if (!modelParams) {\n            const { arch, resModel, fields, context } = genericProps;\n            const parser = new view.ArchParser();\n            const archInfo = parser.parse(arch);\n            const views = config.views || [];\n            modelParams = {\n                allowResequence: archInfo.allowResequence || false,\n                context: context,\n                defaultOrder: archInfo.defaultOrder,\n                fieldNames: archInfo.fieldNames,\n                fieldNamesMarkerPopup: archInfo.fieldNamesMarkerPopup,\n                fields: fields,\n                hasFormView: views.some((view) => view[1] === \"form\"),\n                hideAddress: archInfo.hideAddress || false,\n                hideName: archInfo.hideName || false,\n                hideTitle: archInfo.hideTitle || false,\n                limit: archInfo.limit || 80,\n                numbering: archInfo.routing || false,\n                offset: 0,\n                panelTitle: archInfo.panelTitle || config.getDisplayName() || _t(\"Items\"),\n                resModel: resModel,\n                resPartnerField: archInfo.resPartnerField,\n                routing: archInfo.routing || false,\n            };\n        }\n\n        return {\n            ...genericProps,\n            Model: view.Model,\n            modelParams,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"map\", mapView);\n", "import { getLocalYearAndWeek } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { getActiveActions } from \"@web/views/utils\";\n\nconst DECORATIONS = [\n    \"decoration-danger\",\n    \"decoration-info\",\n    \"decoration-secondary\",\n    \"decoration-success\",\n    \"decoration-warning\",\n];\nconst PARTS = { full: 1, half: 2, quarter: 4 };\nconst SCALES = {\n    day: {\n        // determines subcolumns\n        cellPrecisions: { full: 60, half: 30, quarter: 15 },\n        defaultPrecision: \"full\",\n        time: \"minute\",\n        unitDescription: _t(\"minutes\"),\n\n        // determines columns\n        interval: \"hour\",\n        minimalColumnWidth: 40,\n\n        // determines column groups\n        unit: \"day\",\n        groupHeaderFormatter: (date) => date.toFormat(\"dd MMMM yyyy\"),\n\n        defaultRange: { unit: \"day\", count: 3 },\n    },\n    week: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 192,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"week\",\n        groupHeaderFormatter: formatLocalWeekYear,\n\n        defaultRange: { unit: \"week\", count: 3 },\n    },\n    week_2: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 96,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"week\",\n        groupHeaderFormatter: formatLocalWeekYear,\n\n        defaultRange: { unit: \"week\", count: 6 },\n    },\n    month: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 50,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"month\",\n        groupHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? \"MMM yyyy\" : \"MMMM yyyy\"),\n\n        defaultRange: { unit: \"month\", count: 3 },\n    },\n    month_3: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 18,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"month\",\n        groupHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? \"MMM yyyy\" : \"MMMM yyyy\"),\n\n        defaultRange: { unit: \"month\", count: 6 },\n    },\n    year: {\n        cellPrecisions: { full: 1 },\n        defaultPrecision: \"full\",\n        time: \"month\",\n        unitDescription: _t(\"months\"),\n\n        interval: \"month\",\n        minimalColumnWidth: 60,\n        colHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? \"MMM\" : \"MMMM\"),\n\n        unit: \"year\",\n        groupHeaderFormatter: (date) => date.toFormat(\"yyyy\"),\n\n        defaultRange: { unit: \"year\", count: 1 },\n    },\n};\n\n/**\n * Formats a date to a `'W'W kkkk` datetime string, in the user's locale settings.\n *\n * @param {Date|luxon.DateTime} date\n * @returns {string}\n */\nfunction formatLocalWeekYear(date) {\n    const { year, week } = getLocalYearAndWeek(date);\n    return `W${week} ${year}`;\n}\n\nfunction getPreferedScaleId(scaleId, scales) {\n    // we assume that scales is not empty\n    if (scaleId in scales) {\n        return scaleId;\n    }\n    const scaleIds = Object.keys(SCALES);\n    const index = scaleIds.findIndex((id) => id === scaleId);\n    for (let j = index - 1; j >= 0; j--) {\n        const id = scaleIds[j];\n        if (id in scales) {\n            return id;\n        }\n    }\n    for (let j = index + 1; j < scaleIds.length; j++) {\n        const id = scaleIds[j];\n        if (id in scales) {\n            return id;\n        }\n    }\n}\n\nconst RANGES = {\n    day: { scaleId: \"day\", description: _t(\"Today\") },\n    week: { scaleId: \"week\", description: _t(\"This week\") },\n    month: { scaleId: \"month\", description: _t(\"This month\") },\n    quarter: { scaleId: \"month_3\", description: _t(\"This quarter\") },\n    year: { scaleId: \"year\", description: _t(\"This year\") },\n};\n\nexport class GanttArchParser {\n    parse(arch) {\n        let infoFromRootNode;\n        const decorationFields = [];\n        const popoverArchParams = {\n            displayGenericButtons: true,\n            bodyTemplate: null,\n            footerTemplate: null,\n        };\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"gantt\": {\n                    infoFromRootNode = getInfoFromRootNode(node);\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\");\n                    decorationFields.push(fieldName);\n                    break;\n                }\n                case \"templates\": {\n                    const body = node.querySelector(\"[t-name=gantt-popover]\") || null;\n                    if (body) {\n                        popoverArchParams.bodyTemplate = body.cloneNode(true);\n                        popoverArchParams.bodyTemplate.removeAttribute(\"t-name\");\n                        const footer = popoverArchParams.bodyTemplate.querySelector(\"footer\");\n                        if (footer) {\n                            popoverArchParams.displayGenericButtons = false;\n                            footer.remove();\n                            const footerTemplate = new Document().createElement(\"t\");\n                            footerTemplate.append(...footer.children);\n                            popoverArchParams.footerTemplate = footerTemplate;\n                            const replace = footer.getAttribute(\"replace\");\n                            if (replace && !exprToBoolean(replace)) {\n                                popoverArchParams.displayGenericButtons = true;\n                            }\n                        }\n                    }\n                }\n            }\n        });\n\n        return {\n            ...infoFromRootNode,\n            decorationFields,\n            popoverArchParams,\n        };\n    }\n}\n\nfunction getInfoFromRootNode(rootNode) {\n    const attrs = {};\n    for (const { name, value } of rootNode.attributes) {\n        attrs[name] = value;\n    }\n\n    const { create: canCreate, delete: canDelete, edit: canEdit } = getActiveActions(rootNode);\n    const canCellCreate = exprToBoolean(attrs.cell_create, true) && canCreate;\n    const canPlan = exprToBoolean(attrs.plan, true) && canEdit;\n\n    let consolidationMaxField;\n    let consolidationMaxValue;\n    const consolidationMax = attrs.consolidation_max ? evaluateExpr(attrs.consolidation_max) : {};\n    if (Object.keys(consolidationMax).length > 0) {\n        consolidationMaxField = Object.keys(consolidationMax)[0];\n        consolidationMaxValue = consolidationMax[consolidationMaxField];\n    }\n\n    const consolidationParams = {\n        excludeField: attrs.consolidation_exclude,\n        field: attrs.consolidation,\n        maxField: consolidationMaxField,\n        maxValue: consolidationMaxValue,\n    };\n\n    const dependencyField = attrs.dependency_field || null;\n    const dependencyEnabled = !!dependencyField;\n    const dependencyInvertedField = attrs.dependency_inverted_field || null;\n\n    const allowedScales = [];\n    if (attrs.scales) {\n        for (const key of attrs.scales.split(\",\")) {\n            if (SCALES[key]) {\n                allowedScales.push(key);\n            }\n        }\n    }\n    if (allowedScales.length === 0) {\n        allowedScales.push(...Object.keys(SCALES));\n    }\n\n    let defaultScale = attrs.default_scale;\n    if (defaultScale) {\n        if (!allowedScales.includes(defaultScale) && SCALES[defaultScale]) {\n            allowedScales.push(defaultScale);\n        }\n    } else if (allowedScales.includes(\"month\")) {\n        defaultScale = \"month\";\n    } else {\n        defaultScale = allowedScales[0];\n    }\n\n    // Cell precision\n    const cellPrecisions = {};\n\n    // precision = {'day': 'hour:half', 'week': 'day:half', 'month': 'day', 'year': 'month:quarter'}\n    const precisionAttrs = attrs.precision ? evaluateExpr(attrs.precision) : {};\n    for (const scaleId in SCALES) {\n        if (precisionAttrs[scaleId]) {\n            const precision = precisionAttrs[scaleId].split(\":\"); // hour:half\n            // Note that precision[0] (which is the cell interval) is not\n            // taken into account right now because it is no customizable.\n            if (\n                precision[1] &&\n                Object.keys(SCALES[scaleId].cellPrecisions).includes(precision[1])\n            ) {\n                cellPrecisions[scaleId] = precision[1];\n            }\n        }\n        cellPrecisions[scaleId] ||= SCALES[scaleId].defaultPrecision;\n    }\n\n    const scales = {};\n    for (const scaleId of allowedScales) {\n        const precision = cellPrecisions[scaleId];\n        const referenceScale = SCALES[scaleId];\n        scales[scaleId] = {\n            ...referenceScale,\n            cellPart: PARTS[precision],\n            cellTime: referenceScale.cellPrecisions[precision],\n            id: scaleId,\n            unitDescription: referenceScale.unitDescription.toString(),\n        };\n        // protect SCALES content\n        delete scales[scaleId].cellPrecisions;\n    }\n\n    const ranges = {};\n    for (const rangeId in RANGES) {\n        const referenceRange = RANGES[rangeId];\n        const { groupHeaderFormatter } = SCALES[referenceRange.scaleId];\n        ranges[rangeId] = {\n            ...referenceRange,\n            groupHeaderFormatter,\n            id: rangeId,\n            scaleId: getPreferedScaleId(referenceRange.scaleId, scales),\n            description: referenceRange.description.toString(),\n        };\n    }\n\n    let pillDecorations = null;\n    for (const decoration of DECORATIONS) {\n        if (decoration in attrs) {\n            if (!pillDecorations) {\n                pillDecorations = {};\n            }\n            pillDecorations[decoration] = attrs[decoration];\n        }\n    }\n\n    return {\n        canCellCreate,\n        canCreate,\n        canDelete,\n        canEdit,\n        canPlan,\n        colorField: attrs.color,\n        computePillDisplayName: !!attrs.pill_label,\n        consolidationParams,\n        createAction: attrs.on_create || null,\n        dateStartField: attrs.date_start,\n        dateStopField: attrs.date_stop,\n        defaultGroupBy: attrs.default_group_by ? attrs.default_group_by.split(\",\") : [],\n        defaultRange: attrs.default_range,\n        defaultScale,\n        dependencyEnabled,\n        dependencyField,\n        dependencyInvertedField,\n        disableDrag: exprToBoolean(attrs.disable_drag_drop),\n        displayMode: attrs.display_mode || \"dense\",\n        displayTotalRow: exprToBoolean(attrs.total_row),\n        displayUnavailability: exprToBoolean(attrs.display_unavailability),\n        formViewId: attrs.form_view_id ? parseInt(attrs.form_view_id, 10) : false,\n        offset: attrs.offset,\n        pagerLimit: attrs.groups_limit ? parseInt(attrs.groups_limit, 10) : null,\n        pillDecorations,\n        progressBarFields: attrs.progress_bar ? attrs.progress_bar.split(\",\") : null,\n        progressField: attrs.progress || null,\n        ranges,\n        scales,\n        string: attrs.string || _t(\"Gantt View\").toString(),\n        thumbnails: attrs.thumbnails ? evaluateExpr(attrs.thumbnails) : {},\n    };\n}\n", "import { ViewCompiler } from \"@web/views/view_compiler\";\n\nexport class GanttCompiler extends ViewCompiler {}\nGanttCompiler.OWL_DIRECTIVE_WHITELIST = [\n    ...ViewCompiler.OWL_DIRECTIVE_WHITELIST,\n    \"t-name\",\n    \"t-esc\",\n    \"t-out\",\n    \"t-set\",\n    \"t-value\",\n    \"t-if\",\n    \"t-else\",\n    \"t-elif\",\n    \"t-foreach\",\n    \"t-as\",\n    \"t-key\",\n    \"t-att.*\",\n    \"t-call\",\n    \"t-translation\",\n];\n", "import { Component, onWillRender, useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * @typedef {\"error\" | \"warning\"} ConnectorAlert\n * @typedef {`__connector__${number | \"new\"}`} ConnectorId\n * @typedef {import(\"./gantt_renderer\").Point} Point\n *\n * @typedef ConnectorProps\n * @property {ConnectorId} id\n * @property {ConnectorAlert | null} alert\n * @property {boolean} highlighted\n * @property {boolean} displayButtons\n * @property {Point | () => Point | null} sourcePoint\n * @property {Point | () => Point | null} targetPoint\n *\n * @typedef {Object} PathInfo\n * @property {Point} sourceControlPoint\n * @property {Point} targetControlPoint\n * @property {Point} removeButtonPosition\n *\n * @typedef Point\n * @property {number} [x]\n * @property {number} [y]\n */\n\n/**\n * Gets the stroke's rgba css string corresponding to the provided parameters for both the stroke and its\n * hovered state.\n *\n * @param {number} r [0, 255]\n * @param {number} g [0, 255]\n * @param {number} b [0, 255]\n * @return {{ stroke: string, hoveredStroke: string }} the css colors.\n */\nexport function getStrokeAndHoveredStrokeColor(r, g, b) {\n    return {\n        color: `rgba(${r},${g},${b},0.5)`,\n        highlightedColor: `rgba(${r},${g},${b},1)`,\n    };\n}\n\nexport const COLORS = {\n    default: getStrokeAndHoveredStrokeColor(143, 143, 143),\n    error: getStrokeAndHoveredStrokeColor(211, 65, 59),\n    warning: getStrokeAndHoveredStrokeColor(236, 151, 31),\n    outline: getStrokeAndHoveredStrokeColor(255, 255, 255),\n};\n\n/** @extends {Component<{ reactive: ConnectorProps }, any>} */\nexport class GanttConnector extends Component {\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                id: String,\n                alert: {\n                    type: [{ value: \"error\" }, { value: \"warning\" }, { value: null }],\n                    optional: true,\n                },\n                highlighted: { type: Boolean, optional: true },\n                displayButtons: { type: Boolean, optional: true },\n                sourcePoint: [\n                    { value: null },\n                    Function,\n                    { type: Object, shape: { left: Number, top: Number } },\n                ],\n                targetPoint: [\n                    { value: null },\n                    Function,\n                    { type: Object, shape: { left: Number, top: Number } },\n                ],\n            },\n        },\n        onLeftButtonClick: { type: Function, optional: true },\n        onRemoveButtonClick: { type: Function, optional: true },\n        onRightButtonClick: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        highlighted: false,\n        displayButtons: false,\n    };\n    static template = \"web_gantt.GanttConnector\";\n\n    rootRef = useRef(\"root\");\n    style = {\n        hoverEaseWidth: 10,\n        slackness: 0.9,\n        stroke: { width: 2 },\n        outlineStroke: { width: 1 },\n    };\n\n    get alert() {\n        return this.props.reactive.alert;\n    }\n\n    get displayButtons() {\n        return this.props.reactive.displayButtons;\n    }\n\n    get highlighted() {\n        return this.props.reactive.highlighted;\n    }\n\n    get id() {\n        return this.props.reactive.id;\n    }\n\n    get isNew() {\n        return this.id.endsWith(\"new\");\n    }\n\n    get sourcePoint() {\n        return this.props.reactive.sourcePoint;\n    }\n\n    get targetPoint() {\n        return this.props.reactive.targetPoint;\n    }\n\n    setup() {\n        onWillRender(this.onWillRender);\n\n        useEffect(\n            (el, sourceLeft, sourceTop, targetLeft, targetTop) => {\n                if (!el) {\n                    return;\n                }\n                const { sourceControlPoint, targetControlPoint, removeButtonPosition } =\n                    this.getPathInfo(\n                        { left: sourceLeft, top: sourceTop },\n                        { left: targetLeft, top: targetTop },\n                        this.style.slackness\n                    );\n\n                const drawingCommands = [\n                    `M`,\n                    `${sourceLeft},${sourceTop}`,\n                    `C`,\n                    `${sourceControlPoint.left},${sourceControlPoint.top}`,\n                    `${targetControlPoint.left},${targetControlPoint.top}`,\n                    `${targetLeft},${targetTop}`,\n                ].join(\" \");\n\n                const paths = el.querySelectorAll(\n                    \".o_connector_stroke, .o_connector_stroke_hover_ease\"\n                );\n                for (const path of paths) {\n                    path.setAttribute(\"d\", drawingCommands);\n                }\n\n                const svgButtons = el.querySelector(\".o_connector_stroke_buttons\");\n                if (svgButtons) {\n                    svgButtons.setAttribute(\"x\", removeButtonPosition.left - 24);\n                    svgButtons.setAttribute(\"y\", removeButtonPosition.top - 8);\n                }\n            },\n            () => this.getEffectDependencies()\n        );\n    }\n\n    /**\n     * Refreshes the connector properties from the props.\n     *\n     * @param {ConnectorProps} props\n     */\n    computeStyle({ alert, highlighted }) {\n        const key = highlighted ? \"highlightedColor\" : \"color\";\n        const strokeType = alert || \"default\";\n        this.style = {\n            hoverEaseWidth: 10,\n            slackness: 0.9,\n            stroke: {\n                color: COLORS[strokeType][key],\n                width: 2,\n            },\n            outlineStroke: {\n                color: COLORS.outline[key],\n                width: 1,\n            },\n        };\n    }\n\n    getEffectDependencies() {\n        let sourcePoint = this.sourcePoint || { left: 0, top: 0 };\n        if (typeof sourcePoint === \"function\") {\n            sourcePoint = sourcePoint();\n        }\n        let targetPoint = this.targetPoint || { left: 0, top: 0 };\n        if (typeof targetPoint === \"function\") {\n            targetPoint = targetPoint();\n        }\n        const { x, y } = this.rootRef.el?.getBoundingClientRect() || { x: 0, y: 0 };\n\n        return [\n            this.rootRef.el,\n            sourcePoint.left - x,\n            sourcePoint.top - y,\n            targetPoint.left - x,\n            targetPoint.top - y,\n            this.displayButtons,\n        ];\n    }\n\n    /**\n     * Returns the linear interpolation for a point to be found somewhere on the line startingPoint, endingPoint.\n     *\n     * @param {Point} startingPoint\n     * @param {Point} endingPoint\n     * @param {number} lambda\n     * @returns {Point}\n     */\n    getLinearInterpolation(startingPoint, endingPoint, lambda = 0.5) {\n        return {\n            left: lambda * startingPoint.left + (1 - lambda) * endingPoint.left,\n            top: lambda * startingPoint.top + (1 - lambda) * endingPoint.top,\n        };\n    }\n\n    /**\n     * Returns the parameters of both the single Bezier curve as well as is decomposition into two beziers curves\n     * (which allows to get the middle position of the single Bezier curve) for the provided source, target and\n     * slackness (0 being a straight line).\n     *\n     * @param {Point} sourcePoint\n     * @param {Point} targetPoint\n     * @param {number} slackness [0, 1]\n     * @returns {PathInfo}\n     */\n    getPathInfo(sourcePoint, targetPoint, slackness) {\n        // If the source is on the left of the target, we need to invert the control points.\n        const xDelta = targetPoint.left - sourcePoint.left;\n        const yDelta = targetPoint.top - sourcePoint.top;\n        const directionFactor = Math.sign(xDelta);\n\n        // What follows can be seen as magic numbers. And those are indeed such numbers as they have been determined\n        // by observing their shape while creating short and long connectors. These seems to allow keeping the same\n        // kind of shape amongst short and long connectors.\n        const xInc = 100 + (Math.abs(xDelta) * slackness) / 10;\n        const yInc =\n            Math.abs(yDelta) < 16 && directionFactor === -1 ? 15 - 0.001 * xDelta * slackness : 0;\n\n        const b = {\n            left: sourcePoint.left + xInc,\n            top: sourcePoint.top + yInc,\n        };\n\n        // Prevent having the air pin effect when in creation and having target on the left of the source\n        const c = {\n            left: targetPoint.left + (this.isNew && directionFactor === -1 ? xInc : -xInc),\n            top: targetPoint.top + yInc,\n        };\n\n        const e = this.getLinearInterpolation(sourcePoint, b);\n        const f = this.getLinearInterpolation(b, c);\n        const g = this.getLinearInterpolation(c, targetPoint);\n        const h = this.getLinearInterpolation(e, f);\n        const i = this.getLinearInterpolation(f, g);\n        const j = this.getLinearInterpolation(h, i);\n\n        return {\n            sourceControlPoint: b,\n            targetControlPoint: c,\n            removeButtonPosition: j,\n        };\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    onLeftButtonClick() {\n        if (this.props.onLeftButtonClick) {\n            this.props.onLeftButtonClick();\n        }\n    }\n\n    onRemoveButtonClick() {\n        if (this.props.onRemoveButtonClick) {\n            this.props.onRemoveButtonClick();\n        }\n    }\n\n    onRightButtonClick() {\n        if (this.props.onRightButtonClick) {\n            this.props.onRightButtonClick();\n        }\n    }\n\n    onWillRender() {\n        const key = this.highlighted ? \"highlightedColor\" : \"color\";\n        this.style.stroke.color = COLORS[this.alert || \"default\"][key];\n        this.style.outlineStroke.color = COLORS.outline[key];\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillUnmount, useEffect, useRef, useSubEnv } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { Layout } from \"@web/search/layout\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\n\nexport class GanttController extends Component {\n    static components = {\n        CogMenu,\n        Layout,\n        SearchBar,\n    };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        modelParams: Object,\n        scrollPosition: { type: Object, optional: true },\n    };\n    static template = \"web_gantt.GanttController\";\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        useSubEnv({\n            getCurrentFocusDateCallBackRecorder: new CallbackRecorder(),\n        });\n\n        const rootRef = useRef(\"root\");\n\n        this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);\n        useSetupAction({\n            rootRef,\n            getLocalState: () => {\n                return { metaData: this.model.metaData, displayParams: this.model.displayParams };\n            },\n        });\n\n        onWillUnmount(() => this.closeDialog?.());\n\n        usePager(() => {\n            const { groupedBy, pagerLimit, pagerOffset } = this.model.metaData;\n            const { count } = this.model.data;\n            if (pagerLimit !== null && groupedBy.length) {\n                return {\n                    offset: pagerOffset,\n                    limit: pagerLimit,\n                    total: count,\n                    onUpdate: async ({ offset, limit }) => {\n                        await this.model.updatePagerParams({ offset, limit });\n                    },\n                };\n            }\n        });\n\n        useEffect(\n            (showNoContentHelp) => {\n                if (showNoContentHelp) {\n                    const realRows = [\n                        ...rootRef.el.querySelectorAll(\n                            \".o_gantt_row_header:not(.o_sample_data_disabled)\"\n                        ),\n                    ];\n                    // interactive rows created in extensions (fromServer undefined)\n                    const headerContainerWidth =\n                        rootRef.el.querySelector(\".o_gantt_header_groups\").clientHeight +\n                        rootRef.el.querySelector(\".o_gantt_header_columns\").clientHeight;\n\n                    const offset = realRows.reduce(\n                        (current, el) => current + el.clientHeight,\n                        headerContainerWidth\n                    );\n\n                    const noContentHelperEl = rootRef.el.querySelector(\".o_view_nocontent\");\n                    noContentHelperEl.style.top = `${offset}px`;\n                }\n            },\n            () => [this.showNoContentHelp]\n        );\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    get className() {\n        if (this.env.isSmall) {\n            const classList = (this.props.className || \"\").split(\" \");\n            classList.push(\"o_action_delegate_scroll\");\n            return classList.join(\" \");\n        }\n        return this.props.className;\n    }\n\n    get showNoContentHelp() {\n        return this.model.useSampleModel;\n    }\n\n    /**\n     * @param {Record<string, any>} [context]\n     */\n    create(context) {\n        const { createAction } = this.model.metaData;\n        if (createAction) {\n            this.actionService.doAction(createAction, {\n                additionalContext: context,\n                onClose: () => {\n                    this.model.fetchData();\n                },\n            });\n        } else {\n            this.openDialog({ context });\n        }\n    }\n\n    /**\n     * Opens dialog to add/edit/view a record\n     *\n     * @param {Record<string, any>} props FormViewDialog props\n     * @param {Record<string, any>} [options={}]\n     */\n    openDialog(props, options = {}) {\n        const { canDelete, canEdit, resModel, formViewId: viewId } = this.model.metaData;\n\n        const title = props.title || (props.resId ? _t(\"Open\") : _t(\"Create\"));\n\n        let removeRecord;\n        if (canDelete && props.resId) {\n            removeRecord = () => {\n                return new Promise((resolve) => {\n                    this.dialogService.add(ConfirmationDialog, {\n                        body: _t(\"Are you sure to delete this record?\"),\n                        confirm: async () => {\n                            await this.orm.unlink(resModel, [props.resId]);\n                            resolve();\n                        },\n                        cancel: () => {},\n                    });\n                });\n            };\n        }\n\n        this.closeDialog = this.dialogService.add(\n            FormViewDialog,\n            {\n                title,\n                resModel,\n                viewId,\n                resId: props.resId,\n                size: props.size,\n                mode: canEdit ? \"edit\" : \"readonly\",\n                context: props.context,\n                removeRecord,\n            },\n            {\n                ...options,\n                onClose: () => {\n                    this.closeDialog = null;\n                    this.model.fetchData();\n                },\n            }\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onAddClicked() {\n        const { scale } = this.model.metaData;\n        const focusDate = this.getCurrentFocusDate();\n        const start = focusDate.startOf(scale.unit);\n        const stop = focusDate.endOf(scale.unit).plus({ millisecond: 1 });\n        const context = this.model.getDialogContext({ start, stop, withDefault: true });\n        this.create(context);\n    }\n\n    getCurrentFocusDate() {\n        const { callbacks } = this.env.getCurrentFocusDateCallBackRecorder;\n        if (callbacks.length) {\n            return callbacks[0]();\n        }\n        return this.model.metaData.focusDate;\n    }\n}\n", "import { onWillUnmount, status, useComponent, useEffect, useEnv } from \"@odoo/owl\";\nimport { getEndOfLocalWeek, getStartOfLocalWeek } from \"@web/core/l10n/dates\";\nimport { makePopover, usePopover } from \"@web/core/popover/popover_hook\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { GanttPopoverInDialog } from \"./gantt_popover_in_dialog\";\n\n/** @typedef {luxon.DateTime} DateTime */\n\n/**\n * @param {number} target\n * @param {number[]} values\n * @returns {number}\n */\nfunction closest(target, values) {\n    return values.reduce(\n        (prev, val) => (Math.abs(val - target) < Math.abs(prev - target) ? val : prev),\n        Infinity\n    );\n}\n\n/**\n * Adds a time diff to a date keeping the same value even if the offset changed\n * during the manipulation. This is typically needed with timezones using DayLight\n * Saving offset changes.\n *\n * @example dateAddFixedOffset(luxon.DateTime.local(), { hour: 1 });\n * @param {DateTime} date\n * @param {Record<string, number>} plusParams\n */\nexport function dateAddFixedOffset(date, plusParams) {\n    const shouldApplyOffset = Object.keys(plusParams).some((key) =>\n        /^(hour|minute|second)s?$/i.test(key)\n    );\n    const result = date.plus(plusParams);\n    if (shouldApplyOffset) {\n        const initialOffset = date.offset;\n        const diff = initialOffset - result.offset;\n        if (diff) {\n            const adjusted = result.plus({ minute: diff });\n            return adjusted.offset === initialOffset ? result : adjusted;\n        }\n    }\n    return result;\n}\n\nexport function diffColumn(col1, col2, unit) {\n    return col2.diff(col1, unit).values[`${unit}s`];\n}\n\nexport function getRangeFromDate(rangeId, date) {\n    const startDate = localStartOf(date, rangeId);\n    const stopDate = startDate.plus({ [rangeId]: 1 }).minus({ day: 1 });\n    return { focusDate: date, startDate, stopDate, rangeId };\n}\n\nexport function localStartOf(date, unit) {\n    return unit === \"week\" ? getStartOfLocalWeek(date) : date.startOf(unit);\n}\n\nexport function localEndOf(date, unit) {\n    return unit === \"week\" ? getEndOfLocalWeek(date) : date.endOf(unit);\n}\n\n/**\n * @param {number} cellPart\n * @param {(0 | 1)[]} subSlotUnavailabilities\n * @param {boolean} isToday\n * @returns {string | null}\n */\nexport function getCellColor(cellPart, subSlotUnavailabilities, isToday) {\n    const sum = subSlotUnavailabilities.reduce((acc, d) => acc + d);\n    if (!sum) {\n        return null;\n    }\n    switch (cellPart) {\n        case sum: {\n            return `background-color:${getCellPartColor(sum, isToday)}`;\n        }\n        case 2: {\n            const [c0, c1] = subSlotUnavailabilities.map((d) => getCellPartColor(d, isToday));\n            return `background:linear-gradient(90deg,${c0}49%,${c1}50%)`;\n        }\n        case 4: {\n            const [c0, c1, c2, c3] = subSlotUnavailabilities.map((d) =>\n                getCellPartColor(d, isToday)\n            );\n            return `background:linear-gradient(90deg,${c0}24%,${c1}25%,${c1}49%,${c2}50%,${c2}74%,${c3}75%)`;\n        }\n    }\n}\n\n/**\n * @param {0 | 1} availability\n * @param {boolean} isToday\n * @returns {string}\n */\nexport function getCellPartColor(availability, isToday) {\n    if (availability) {\n        return \"var(--Gantt__DayOff-background-color)\";\n    } else if (isToday) {\n        return \"var(--Gantt__DayOffToday-background-color)\";\n    } else {\n        return \"var(--Gantt__Day-background-color)\";\n    }\n}\n\n/**\n * @param {number | [number, string]} value\n * @returns {number}\n */\nexport function getColorIndex(value) {\n    if (typeof value === \"number\") {\n        return Math.round(value) % NB_GANTT_RECORD_COLORS;\n    } else if (Array.isArray(value)) {\n        return value[0] % NB_GANTT_RECORD_COLORS;\n    }\n    return 0;\n}\n\n/**\n * Intervals are supposed to intersect (intersection duration >= 1 milliseconds)\n *\n * @param {[DateTime, DateTime]} interval\n * @param {[DateTime, DateTime]} otherInterval\n * @returns {[DateTime, DateTime]}\n */\nexport function getIntersection(interval, otherInterval) {\n    const [start, end] = interval;\n    const [otherStart, otherEnd] = otherInterval;\n    return [start >= otherStart ? start : otherStart, end <= otherEnd ? end : otherEnd];\n}\n\n/**\n * Computes intersection of a closed interval with a union of closed intervals ordered and disjoint\n * = a union of intersections\n *\n * @param {[DateTime, DateTime]} interval\n * @param {[DateTime, DateTime]} intervals\n * @returns {[DateTime, DateTime][]}\n */\nexport function getUnionOfIntersections(interval, intervals) {\n    const [start, end] = interval;\n    const intersecting = intervals.filter((otherInterval) => {\n        const [otheStart, otherEnd] = otherInterval;\n        return otherEnd > start && end > otheStart;\n    });\n    const len = intersecting.length;\n    if (len === 0) {\n        return [];\n    }\n    const union = [];\n    const first = getIntersection(interval, intersecting[0]);\n    union.push(first);\n    if (len >= 2) {\n        const last = getIntersection(interval, intersecting[len - 1]);\n        union.push(...intersecting.slice(1, len - 1), last);\n    }\n    return union;\n}\n\n/**\n * @param {Object} params\n * @param {Ref<HTMLElement>} params.ref\n * @param {string} params.selector\n * @param {string} params.related\n * @param {string} params.className\n */\nexport function useMultiHover({ ref, selector, related, className }) {\n    /**\n     * @param {HTMLElement} el\n     */\n    const findSiblings = (el) =>\n        ref.el.querySelectorAll(\n            related\n                .map((attr) => `[${attr}='${el.getAttribute(attr).replace(/'/g, \"\\\\'\")}']`)\n                .join(\"\")\n        );\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    const onPointerEnter = (ev) => {\n        for (const sibling of findSiblings(ev.target)) {\n            sibling.classList.add(...classList);\n            classedEls.add(sibling);\n        }\n    };\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    const onPointerLeave = (ev) => {\n        for (const sibling of findSiblings(ev.target)) {\n            sibling.classList.remove(...classList);\n            classedEls.delete(sibling);\n        }\n    };\n\n    const classList = className.split(/\\s+/g);\n    const classedEls = new Set();\n\n    useEffect(\n        (...targets) => {\n            if (targets.length) {\n                for (const target of targets) {\n                    target.addEventListener(\"pointerenter\", onPointerEnter);\n                    target.addEventListener(\"pointerleave\", onPointerLeave);\n                }\n                return () => {\n                    for (const el of classedEls) {\n                        el.classList.remove(...classList);\n                    }\n                    classedEls.clear();\n                    for (const target of targets) {\n                        target.removeEventListener(\"pointerenter\", onPointerEnter);\n                        target.removeEventListener(\"pointerleave\", onPointerLeave);\n                    }\n                };\n            }\n        },\n        () => [...ref.el.querySelectorAll(selector)]\n    );\n}\n\nconst NB_GANTT_RECORD_COLORS = 12;\n\nfunction getElementCenter(el) {\n    const { x, y, width, height } = el.getBoundingClientRect();\n    return {\n        x: x + width / 2,\n        y: y + height / 2,\n    };\n}\n\n// Resizable hook handles\n\nconst HANDLE_CLASS_START = \"o_handle_start\";\nconst HANDLE_CLASS_END = \"o_handle_end\";\nconst handles = {\n    start: document.createElement(\"div\"),\n    end: document.createElement(\"div\"),\n};\n\n// Draggable hooks\n\nexport const useGanttConnectorDraggable = makeDraggableHook({\n    name: \"useGanttConnectorDraggable\",\n    acceptedParams: {\n        parentWrapper: [String],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.parentWrapper = params.parentWrapper;\n        ctx.followCursor = false;\n    },\n    onDragStart: ({ ctx, addStyle }) => {\n        const { current } = ctx;\n        const parent = current.element.closest(ctx.parentWrapper);\n        if (!parent) {\n            return;\n        }\n        for (const otherParent of ctx.ref.el.querySelectorAll(ctx.parentWrapper)) {\n            if (otherParent !== parent) {\n                addStyle(otherParent, { pointerEvents: \"auto\" });\n            }\n        }\n        return { sourcePill: parent, ...current.connectorCenter };\n    },\n    onDrag: ({ ctx }) => {\n        ctx.current.connectorCenter = getElementCenter(ctx.current.element);\n        return pick(ctx.current, \"connectorCenter\");\n    },\n    onDragEnd: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDrop: ({ ctx, target }) => {\n        const { current } = ctx;\n        const parent = current.element.closest(ctx.parentWrapper);\n        const targetParent = target.closest(ctx.parentWrapper);\n        if (!targetParent || targetParent === parent) {\n            return;\n        }\n        return { target: targetParent };\n    },\n    onWillStartDrag: ({ ctx }) => {\n        ctx.current.connectorCenter = getElementCenter(ctx.current.element);\n    },\n});\n\nfunction getCoordinate(style, name) {\n    return +style.getPropertyValue(name).slice(1);\n}\n\nfunction getColumnStart(style) {\n    return getCoordinate(style, \"grid-column-start\");\n}\n\nfunction getColumnEnd(style) {\n    return getCoordinate(style, \"grid-column-end\");\n}\n\nexport const useGanttDraggable = makeDraggableHook({\n    name: \"useGanttDraggable\",\n    acceptedParams: {\n        cells: [String, Function],\n        cellDragClassName: [String, Function],\n        ghostClassName: [String, Function],\n        hoveredCell: [Object],\n        addStickyCoordinates: [Function],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.cellSelector = params.cells;\n        ctx.ghostClassName = params.ghostClassName;\n        ctx.cellDragClassName = params.cellDragClassName;\n        ctx.hoveredCell = params.hoveredCell;\n        ctx.addStickyCoordinates = params.addStickyCoordinates;\n    },\n    onDragStart({ ctx }) {\n        const { current, ghostClassName } = ctx;\n        current.element.before(current.placeHolder);\n        if (ghostClassName) {\n            current.placeHolder.classList.add(ghostClassName);\n        }\n        return { pill: current.element };\n    },\n    onDrag({ ctx, addStyle }) {\n        const { cellSelector, current, hoveredCell } = ctx;\n        let { el: cell, part } = hoveredCell;\n\n        const isDifferentCell = cell !== current.cell.el;\n        const isDifferentPart = part !== current.cell.part;\n\n        if (cell && !cell.matches(cellSelector)) {\n            cell = null; // Not a cell\n        }\n\n        current.cell.el = cell;\n        current.cell.part = part;\n\n        if (cell) {\n            // Recompute cell style if in a different cell\n            if (isDifferentCell) {\n                const style = getComputedStyle(cell);\n                current.cell.gridRow = style.getPropertyValue(\"grid-row\");\n                current.cell.gridColumnStart = getColumnStart(style) + current.gridColumnOffset;\n            }\n            // Assign new grid coordinates if in different cell or different cell part\n            if (isDifferentCell || isDifferentPart) {\n                const { pillSpan } = current;\n                const { gridRow, gridColumnStart: start } = current.cell;\n                const gridColumnStart = clamp(start + part, 1, current.maxGridColumnStart);\n                const gridColumnEnd = gridColumnStart + pillSpan;\n\n                addStyle(current.cellGhost, {\n                    gridRow,\n                    gridColumn: `c${gridColumnStart} / c${gridColumnEnd}`,\n                });\n\n                const [gridRowStart, gridRowEnd] = /r(\\d+) \\/ r(\\d+)/g.exec(gridRow).slice(1);\n                ctx.addStickyCoordinates(\n                    [gridRowStart, gridRowEnd],\n                    [gridColumnStart, gridColumnEnd]\n                );\n                current.cell.col = gridColumnStart;\n            }\n        } else {\n            current.cell.col = null;\n        }\n\n        // Attach or remove cell ghost\n        if (isDifferentCell) {\n            if (cell) {\n                cell.after(current.cellGhost);\n            } else {\n                current.cellGhost.remove();\n            }\n        }\n\n        return { pill: current.element };\n    },\n    onDragEnd({ ctx }) {\n        return { pill: ctx.current.element };\n    },\n    onDrop({ ctx }) {\n        const { cell, element, initialCol } = ctx.current;\n        if (cell.col !== null) {\n            return {\n                pill: element,\n                cell: cell.el,\n                diff: cell.col - initialCol,\n            };\n        }\n    },\n    onWillStartDrag({ ctx, addCleanup, addClass }) {\n        const { current } = ctx;\n        const { el: cell, part } = ctx.hoveredCell;\n\n        current.placeHolder = current.element.cloneNode(true);\n        current.cellGhost = document.createElement(\"div\");\n        current.cellGhost.className = ctx.cellDragClassName;\n        current.cell = { el: null, index: null, part: 0 };\n\n        const gridStyle = getComputedStyle(cell.parentElement);\n        const pillStyle = getComputedStyle(current.element);\n        const cellStyle = getComputedStyle(cell);\n\n        const gridTemplateColumns = gridStyle.getPropertyValue(\"grid-template-columns\");\n        const pGridColumnStart = getColumnStart(pillStyle);\n        const pGridColumnEnd = getColumnEnd(pillStyle);\n        const cGridColumnStart = getColumnStart(cellStyle) + part;\n\n        let highestGridCol;\n        for (const e of gridTemplateColumns.split(/\\s+/).reverse()) {\n            const res = /\\[c(\\d+)\\]/g.exec(e);\n            if (res) {\n                highestGridCol = +res[1];\n                break;\n            }\n        }\n\n        const pillSpan = pGridColumnEnd - pGridColumnStart;\n\n        current.initialCol = pGridColumnStart;\n        current.maxGridColumnStart = highestGridCol - pillSpan;\n        current.gridColumnOffset = pGridColumnStart - cGridColumnStart;\n        current.pillSpan = pillSpan;\n\n        addClass(ctx.ref.el, \"pe-auto\");\n        addCleanup(() => {\n            current.placeHolder.remove();\n            current.cellGhost.remove();\n        });\n    },\n});\n\nexport const useGanttUndraggable = makeDraggableHook({\n    name: \"useGanttUndraggable\",\n    onDragStart({ ctx }) {\n        return { pill: ctx.current.element };\n    },\n    onDragEnd({ ctx }) {\n        return { pill: ctx.current.element };\n    },\n    onWillStartDrag({ ctx, addCleanup, addClass, addStyle, getRect }) {\n        const { x, y, width, height } = getRect(ctx.current.element);\n        ctx.current.container = document.createElement(\"div\");\n\n        addClass(ctx.ref.el, \"pe-auto\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            left: `${x}px`,\n            top: `${y}px`,\n            width: `${width}px`,\n            height: `${height}px`,\n        });\n\n        ctx.current.element.after(ctx.current.container);\n        addCleanup(() => ctx.current.container.remove());\n    },\n});\n\nexport const useGanttResizable = makeDraggableHook({\n    name: \"useGanttResizable\",\n    requiredParams: [\"handles\"],\n    acceptedParams: {\n        innerPills: [String, Function],\n        handles: [String, Function],\n        hoveredCell: [Object],\n        rtl: [Boolean, Function],\n        cells: [String, Function],\n        precision: [Number, Function],\n        showHandles: [Function],\n    },\n    onComputeParams({ ctx, params, addCleanup, addEffectCleanup, getRect }) {\n        const onElementPointerEnter = (ev) => {\n            if (ctx.dragging || ctx.willDrag) {\n                return;\n            }\n\n            const pill = ev.target;\n            const innerPill = pill.querySelector(params.innerPills);\n\n            const pillRect = getRect(innerPill);\n\n            for (const el of Object.values(handles)) {\n                el.style.height = `${pillRect.height}px`;\n            }\n\n            const showHandles = params.showHandles ? params.showHandles(pill) : {};\n            if (\"start\" in showHandles && !showHandles.start) {\n                handles.start.remove();\n            } else {\n                innerPill.appendChild(handles.start);\n            }\n            if (\"end\" in showHandles && !showHandles.end) {\n                handles.end.remove();\n            } else {\n                innerPill.appendChild(handles.end);\n            }\n        };\n\n        const onElementPointerLeave = () => {\n            const remove = () => Object.values(handles).forEach((h) => h.remove());\n            if (ctx.dragging || ctx.current.element) {\n                addCleanup(remove);\n            } else {\n                remove();\n            }\n        };\n\n        ctx.cellSelector = params.cells;\n        ctx.hoveredCell = params.hoveredCell;\n        ctx.precision = params.precision;\n        ctx.rtl = params.rtl;\n\n        for (const el of ctx.ref.el.querySelectorAll(params.elements)) {\n            el.addEventListener(\"pointerenter\", onElementPointerEnter);\n            el.addEventListener(\"pointerleave\", onElementPointerLeave);\n            addEffectCleanup(() => {\n                el.removeEventListener(\"pointerenter\", onElementPointerEnter);\n                el.removeEventListener(\"pointerleave\", onElementPointerLeave);\n            });\n        }\n\n        handles.start.className = `${params.handles} ${HANDLE_CLASS_START}`;\n        handles.start.style.cursor = `${params.rtl ? \"e\" : \"w\"}-resize`;\n\n        handles.end.className = `${params.handles} ${HANDLE_CLASS_END}`;\n        handles.end.style.cursor = `${params.rtl ? \"w\" : \"e\"}-resize`;\n\n        // Override \"full\" and \"element\" selectors: we want the draggable feature\n        // to apply to the handles\n        ctx.pillSelector = ctx.elementSelector;\n        ctx.fullSelector = ctx.elementSelector = `.${params.handles}`;\n\n        // Force the handles to stay in place\n        ctx.followCursor = false;\n    },\n    onDragStart({ ctx, addStyle }) {\n        addStyle(ctx.current.pill, { zIndex: 15 });\n        return { pill: ctx.current.pill };\n    },\n    onDrag({ ctx, addStyle, getRect }) {\n        const { cellSelector, current, hoveredCell, pointer, precision, rtl, ref } = ctx;\n        let { el: cell, part } = hoveredCell;\n\n        const point = [pointer.x, current.initialPosition.y];\n        if (!cell) {\n            let rect;\n            cell = document.elementsFromPoint(...point).find((el) => el.matches(cellSelector));\n            if (!cell) {\n                const cells = Array.from(ref.el.querySelectorAll(\".o_gantt_cells .o_gantt_cell\"));\n                if (pointer.x < current.initialPosition.x) {\n                    cell = rtl ? cells.at(-1) : cells[0];\n                } else {\n                    cell = rtl ? cells[0] : cells.at(-1);\n                }\n                rect = getRect(cell);\n                point[0] = rtl ? rect.right - 1 : rect.left + 1;\n            } else {\n                rect = getRect(cell);\n            }\n            const x = Math.floor(rect.x);\n            const width = Math.floor(rect.width);\n            part = Math.floor((point[0] - x) / (width / precision));\n        }\n\n        const cellStyle = getComputedStyle(cell);\n        const cGridColStart = getColumnStart(cellStyle);\n\n        const { x, width } = getRect(cell);\n        const coef = ((rtl ? -1 : 1) * width) / precision;\n        const startBorder = (rtl ? x + width : x) + part * coef;\n        const endBorder = startBorder + coef;\n\n        const theClosest = closest(point[0], [startBorder, endBorder]);\n\n        let diff =\n            cGridColStart +\n            part +\n            (theClosest === startBorder ? 0 : 1) -\n            (current.isStart ? current.firstCol : current.lastCol);\n\n        if (diff === current.lastDiff) {\n            return;\n        }\n\n        if (current.isStart) {\n            diff = Math.min(diff, current.initialDiff - 1);\n            addStyle(current.pill, { \"grid-column-start\": `c${current.firstCol + diff}` });\n        } else {\n            diff = Math.max(diff, 1 - current.initialDiff);\n            addStyle(current.pill, { \"grid-column-end\": `c${current.lastCol + diff}` });\n        }\n        current.lastDiff = diff;\n\n        const isLeftHandle = rtl ? !current.isStart : current.isStart;\n        const grabbedHandle = isLeftHandle ? \"left\" : \"right\";\n        diff = current.isStart ? -diff : diff;\n        return { pill: current.pill, grabbedHandle, diff };\n    },\n    onDragEnd({ ctx }) {\n        const { current, pillSelector } = ctx;\n        const pill = current.element.closest(pillSelector);\n        return { pill };\n    },\n    onDrop({ ctx }) {\n        const { current } = ctx;\n\n        if (!current.lastDiff) {\n            return;\n        }\n\n        const direction = current.isStart ? \"start\" : \"end\";\n        return { pill: current.pill, diff: current.lastDiff, direction };\n    },\n    onWillStartDrag({ ctx, addClass }) {\n        const { current, pillSelector } = ctx;\n\n        const pill = ctx.current.element.closest(pillSelector);\n        current.pill = pill;\n\n        const pillStyle = getComputedStyle(pill);\n        current.firstCol = getColumnStart(pillStyle);\n        current.lastCol = getColumnEnd(pillStyle);\n        current.initialDiff = current.lastCol - current.firstCol;\n\n        ctx.cursor = getComputedStyle(current.element).cursor;\n\n        current.isStart = current.element.classList.contains(HANDLE_CLASS_START);\n\n        addClass(ctx.ref.el, \"pe-auto\");\n    },\n});\n\nfunction getCellsOnRow(refEl, rowId) {\n    return refEl.querySelectorAll(\n        `.o_gantt_cell:not(.o_gantt_group)[data-row-id='${CSS.escape(rowId)}']`\n    );\n}\n\nfunction getMinMax(a, b) {\n    return a <= b ? [a, b] : [b, a];\n}\n\nexport const useGanttSelectable = makeDraggableHook({\n    name: \"useGanttSelectable\",\n    acceptedParams: {\n        hoveredCell: [Object],\n        rtl: [Boolean, Function],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.followCursor = false;\n        ctx.hoveredCell = params.hoveredCell;\n        ctx.rtl = params.rtl;\n    },\n    onDrag({ ctx, addClass, getRect, removeClass }) {\n        const { current, hoveredCell, pointer, ref, rtl } = ctx;\n        let { el: cell } = hoveredCell;\n        if (!cell) {\n            const point = [pointer.x, current.initialPosition.y];\n            cell = document.elementsFromPoint(...point).find((el) => el.matches(\".o_gantt_cell\"));\n            if (!cell) {\n                const cells = Array.from(ref.el.querySelectorAll(\".o_gantt_cells .o_gantt_cell\"));\n                if (pointer.x < current.initialPosition.x) {\n                    cell = rtl ? cells.at(-1) : cells[0];\n                } else {\n                    cell = rtl ? cells[0] : cells.at(-1);\n                }\n            }\n        }\n        const col = +cell.dataset.col;\n        const lastSelectedCol = current.lastSelectedCol;\n        current.lastSelectedCol = col;\n        if (lastSelectedCol === col) {\n            return;\n        }\n        const [startCol, stopCol] = getMinMax(current.initialCol, col);\n        for (const cell of getCellsOnRow(ref.el, current.rowId)) {\n            const cellCol = +cell.dataset.col;\n            if (cellCol < startCol || cellCol > stopCol) {\n                removeClass(cell, \"o_drag_hover\");\n            } else {\n                addClass(cell, \"o_drag_hover\");\n            }\n        }\n    },\n    onDrop({ ctx }) {\n        const { current } = ctx;\n        const { rowId, initialCol, lastSelectedCol } = current;\n        const [startCol, stopCol] = getMinMax(initialCol, lastSelectedCol);\n        return { rowId, startCol, stopCol };\n    },\n    onWillStartDrag({ ctx, addClass }) {\n        const { current, hoveredCell, ref } = ctx;\n        const { el: cell } = hoveredCell;\n        current.rowId = cell.dataset.rowId;\n        current.initialCol = +cell.dataset.col;\n        addClass(ref.el, \"pe-auto\");\n        addClass(cell, \"pe-auto\");\n    },\n});\n\n/**\n * Same as usePopover, but replaces the popover by a dialog when display size is small.\n *\n * @param {typeof import(\"@odoo/owl\").Component} component\n * @param {import(\"@web/core/popover/popover_service\").PopoverServiceAddOptions} [options]\n * @returns {import(\"@web/core/popover/popover_hook\").PopoverHookReturnType}\n */\nexport function useGanttResponsivePopover(dialogTitle, component, options = {}) {\n    const dialogService = useService(\"dialog\");\n    const env = useEnv();\n    const owner = useComponent();\n    const popover = usePopover(component, options);\n    const onClose = () => {\n        if (status(owner) !== \"destroyed\") {\n            options.onClose?.();\n        }\n    };\n    const dialogAddFn = (_, comp, props, options) => dialogService.add(comp, props, options);\n    const popoverInDialog = makePopover(dialogAddFn, GanttPopoverInDialog, { onClose });\n    const ganttReponsivePopover = {\n        open: (target, props) => {\n            if (env.isSmall) {\n                popoverInDialog.open(target, {\n                    component: component,\n                    componentProps: props,\n                    dialogTitle,\n                });\n            } else {\n                popover.open(target, props);\n            }\n        },\n        close: () => {\n            popover.close();\n            popoverInDialog.close();\n        },\n        get isOpen() {\n            return popover.isOpen || popoverInDialog.isOpen;\n        },\n    };\n    onWillUnmount(ganttReponsivePopover.close);\n    return ganttReponsivePopover;\n}\n", "import { registry } from \"@web/core/registry\";\n\nfunction _mockGetGanttData(_, { model, kwargs }) {\n    const lazy = !kwargs.limit && !kwargs.offset && kwargs.groupby.length === 1;\n    const { groups, length } = this.mockWebReadGroup(model, {\n        ...kwargs,\n        lazy,\n        fields: [\"__record_ids:array_agg(id)\"],\n    });\n\n    const recordIds = [];\n    for (const group of groups) {\n        recordIds.push(...(group.__record_ids || []));\n    }\n\n    const { records } = this.mockWebSearchReadUnity(model, [], {\n        domain: [[\"id\", \"in\", recordIds]],\n        context: kwargs.context,\n        specification: kwargs.read_specification,\n    });\n\n    const unavailabilities = {};\n    for (const fieldName of kwargs.unavailability_fields || []) {\n        unavailabilities[fieldName] = {};\n    }\n\n    const progress_bars = {};\n    for (const fieldName of kwargs.progress_bar_fields || []) {\n        progress_bars[fieldName] = {};\n    }\n\n    return { groups, length, records, unavailabilities, progress_bars };\n}\n\nregistry.category(\"mock_server\").add(\"get_gantt_data\", _mockGetGanttData);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { groupBy, unique } from \"@web/core/utils/arrays\";\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { Model } from \"@web/model/model\";\nimport { parseServerValue } from \"@web/model/relational_model/utils\";\nimport { formatFloatTime, formatPercentage } from \"@web/views/fields/formatters\";\nimport { getRangeFromDate, localStartOf } from \"./gantt_helpers\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {luxon.DateTime} DateTime\n * @typedef {`[{${string}}]`} RowId\n * @typedef {import(\"./gantt_arch_parser\").Scale} Scale\n * @typedef {import(\"./gantt_arch_parser\").ScaleId} ScaleId\n *\n * @typedef ConsolidationParams\n * @property {string} excludeField\n * @property {string} field\n * @property {string} [maxField]\n * @property {string} [maxValue]\n *\n * @typedef Data\n * @property {Record<string, any>[]} records\n * @property {Row[]} rows\n *\n * @typedef Field\n * @property {string} name\n * @property {string} type\n * @property {[any, string][]} [selection]\n *\n * @typedef MetaData\n * @property {ConsolidationParams} consolidationParams\n * @property {string} dateStartField\n * @property {string} dateStopField\n * @property {string[]} decorationFields\n * @property {ScaleId} defaultScale\n * @property {string} dependencyField\n * @property {boolean} dynamicRange\n * @property {Record<string, Field>} fields\n * @property {DateTime} focusDate\n * @property {number | false} formViewId\n * @property {string[]} groupedBy\n * @property {Element | null} popoverTemplate\n * @property {string} resModel\n * @property {Scale} scale\n * @property {Scale[]} scales\n * @property {DateTime} startDate\n * @property {DateTime} stopDate\n *\n * @typedef ProgressBar\n * @property {number} value_formatted\n * @property {number} max_value_formatted\n * @property {number} ratio\n * @property {string} warning\n *\n * @typedef Row\n * @property {RowId} id\n * @property {boolean} consolidate\n * @property {boolean} fromServer\n * @property {string[]} groupedBy\n * @property {string} groupedByField\n * @property {number} groupLevel\n * @property {string} name\n * @property {number[]} recordIds\n * @property {ProgressBar} [progressBar]\n * @property {number | false} resId\n * @property {Row[]} [rows]\n */\n\nfunction firstColumnBefore(date, unit) {\n    return localStartOf(date, unit);\n}\n\nfunction firstColumnAfter(date, unit) {\n    const start = localStartOf(date, unit);\n    if (date.equals(start)) {\n        return date;\n    }\n    return start.plus({ [unit]: 1 });\n}\n\n/**\n * @param {Record<string, Field>} fields\n * @param {Record<string, any>} values\n */\nexport function parseServerValues(fields, values) {\n    /** @type {Record<string, any>} */\n    const parsedValues = {};\n    if (!values) {\n        return parsedValues;\n    }\n    for (const fieldName in values) {\n        const field = fields[fieldName];\n        const value = values[fieldName];\n        switch (field.type) {\n            case \"date\": {\n                parsedValues[fieldName] = value ? deserializeDate(value) : false;\n                break;\n            }\n            case \"datetime\": {\n                parsedValues[fieldName] = value ? deserializeDateTime(value) : false;\n                break;\n            }\n            case \"selection\": {\n                if (value === false) {\n                    // process selection: convert false to 0, if 0 is a valid key\n                    const hasKey0 = field.selection.some((option) => option[0] === 0);\n                    parsedValues[fieldName] = hasKey0 ? 0 : value;\n                } else {\n                    parsedValues[fieldName] = value;\n                }\n                break;\n            }\n            case \"html\": {\n                parsedValues[fieldName] = parseServerValue(field, value);\n                break;\n            }\n            case \"many2one\": {\n                parsedValues[fieldName] = value ? [value.id, value.display_name] : false;\n                break;\n            }\n            default: {\n                parsedValues[fieldName] = value;\n            }\n        }\n    }\n    return parsedValues;\n}\n\nexport class GanttModel extends Model {\n    static services = [\"notification\"];\n\n    setup(params, services) {\n        this.notification = services.notification;\n\n        /** @type {Data} */\n        this.data = {};\n        /** @type {MetaData} */\n        this.metaData = params.metaData;\n        this.displayParams = params.displayParams;\n\n        this.searchParams = null;\n\n        /** @type {Set<RowId>} */\n        this.closedRows = new Set();\n\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.mutex = new Mutex();\n        /** @type {MetaData | null} */\n        this._nextMetaData = null;\n    }\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n\n        const metaData = this._buildMetaData();\n\n        const params = {\n            groupedBy: this._getGroupedBy(metaData, searchParams),\n            pagerOffset: 0,\n        };\n\n        if (!metaData.scale || !metaData.startDate || !metaData.stopDate) {\n            Object.assign(\n                params,\n                this._getInitialRangeParams(this._buildMetaData(params), searchParams)\n            );\n        }\n\n        await this._fetchData(this._buildMetaData(params));\n    }\n\n    //-------------------------------------------------------------------------\n    // Public\n    //-------------------------------------------------------------------------\n\n    collapseRows() {\n        const collapse = (rows) => {\n            for (const row of rows) {\n                this.closedRows.add(row.id);\n                if (row.rows) {\n                    collapse(row.rows);\n                }\n            }\n        };\n        collapse(this.data.rows);\n        this.notify();\n    }\n\n    /**\n     * Create a copy of a task with defaults determined by schedule.\n     *\n     * @param {number} id\n     * @param {Record<string, any>} schedule\n     * @param {(result: any) => any} [callback]\n     */\n    copy(id, schedule, callback) {\n        const { resModel } = this.metaData;\n        const { context } = this.searchParams;\n        const data = this._scheduleToData(schedule);\n        return this.mutex.exec(async () => {\n            const result = await this.orm.call(resModel, \"copy\", [[id]], {\n                context,\n                default: data,\n            });\n            if (callback) {\n                callback(result[0]);\n            }\n            this.fetchData();\n        });\n    }\n\n    /**\n     * Adds a dependency between masterId and slaveId (slaveId depends\n     * on masterId).\n     *\n     * @param {number} masterId\n     * @param {number} slaveId\n     */\n    async createDependency(masterId, slaveId) {\n        const { dependencyField, resModel } = this.metaData;\n        const writeCommand = {\n            [dependencyField]: [x2ManyCommands.link(masterId)],\n        };\n        await this.mutex.exec(() => this.orm.write(resModel, [slaveId], writeCommand));\n        await this.fetchData();\n    }\n\n    dateStartFieldIsDate(metaData = this.metaData) {\n        return metaData?.fields[metaData.dateStartField].type === \"date\";\n    }\n\n    dateStopFieldIsDate(metaData = this.metaData) {\n        return metaData?.fields[metaData.dateStopField].type === \"date\";\n    }\n\n    expandRows() {\n        this.closedRows.clear();\n        this.notify();\n    }\n\n    async fetchData(params) {\n        await this._fetchData(this._buildMetaData(params));\n        this.useSampleModel = false;\n        this.notify();\n    }\n\n    /**\n     * @param {Object} params\n     * @param {RowId} [params.rowId]\n     * @param {DateTime} [params.start]\n     * @param {DateTime} [params.stop]\n     * @param {boolean} [params.withDefault]\n     * @returns {Record<string, any>}\n     */\n    getDialogContext(params) {\n        /** @type {Record<string, any>} */\n        const context = { ...this.getSchedule(params) };\n\n        if (params.withDefault) {\n            for (const k in context) {\n                context[sprintf(\"default_%s\", k)] = context[k];\n            }\n        }\n\n        return Object.assign({}, this.searchParams.context, context);\n    }\n\n    /**\n     * @param {Object} params\n     * @param {RowId} [params.rowId]\n     * @param {DateTime} [params.start]\n     * @param {DateTime} [params.stop]\n     * @returns {Record<string, any>}\n     */\n    getSchedule({ rowId, start, stop } = {}) {\n        const { dateStartField, dateStopField, fields, groupedBy } = this.metaData;\n\n        /** @type {Record<string, any>} */\n        const schedule = {};\n\n        if (start) {\n            schedule[dateStartField] = this.dateStartFieldIsDate()\n                ? serializeDate(start)\n                : serializeDateTime(start);\n        }\n        if (stop && dateStartField !== dateStopField) {\n            schedule[dateStopField] = this.dateStopFieldIsDate()\n                ? serializeDate(stop)\n                : serializeDateTime(stop);\n        }\n        if (rowId) {\n            const group = Object.assign({}, ...JSON.parse(rowId));\n            for (const fieldName of groupedBy) {\n                if (fieldName in group) {\n                    const value = group[fieldName];\n                    if (Array.isArray(value)) {\n                        const { type } = fields[fieldName];\n                        schedule[fieldName] = type === \"many2many\" ? [value[0]] : value[0];\n                    } else {\n                        schedule[fieldName] = value;\n                    }\n                }\n            }\n        }\n\n        return schedule;\n    }\n\n    /**\n     * @override\n     * @returns {boolean}\n     */\n    hasData() {\n        return Boolean(this.data.records.length);\n    }\n\n    /**\n     * @param {RowId} rowId\n     * @returns {boolean}\n     */\n    isClosed(rowId) {\n        return this.closedRows.has(rowId);\n    }\n\n    /**\n     * Removes the dependency between masterId and slaveId (slaveId is no\n     * more dependent on masterId).\n     *\n     * @param {number} masterId\n     * @param {number} slaveId\n     */\n    async removeDependency(masterId, slaveId) {\n        const { dependencyField, resModel } = this.metaData;\n        const writeCommand = {\n            [dependencyField]: [x2ManyCommands.unlink(masterId)],\n        };\n        await this.mutex.exec(() => this.orm.write(resModel, [slaveId], writeCommand));\n        await this.fetchData();\n    }\n\n    /**\n     * Removes from 'data' the fields holding the same value as the records targetted\n     * by 'ids'.\n     *\n     * @template {Record<string, any>} T\n     * @param {T} data\n     * @param {number[]} ids\n     * @returns {Partial<T>}\n     */\n    removeRedundantData(data, ids) {\n        const records = this.data.records.filter((rec) => ids.includes(rec.id));\n        if (!records.length) {\n            return data;\n        }\n\n        /**\n         *\n         * @param {Record<string, any>} record\n         * @param {Field} field\n         */\n        const isSameValue = (record, { name, type }) => {\n            const recordValue = record[name];\n            let newValue = data[name];\n            if (Array.isArray(newValue)) {\n                [newValue] = newValue;\n            }\n            if (Array.isArray(recordValue)) {\n                if (type === \"many2many\") {\n                    return recordValue.includes(newValue);\n                } else {\n                    return recordValue[0] === newValue;\n                }\n            } else if (type === \"date\") {\n                return serializeDate(recordValue) === newValue;\n            } else if (type === \"datetime\") {\n                return serializeDateTime(recordValue) === newValue;\n            } else {\n                return recordValue === newValue;\n            }\n        };\n\n        /** @type {Partial<T>} */\n        const trimmed = { ...data };\n\n        for (const fieldName in data) {\n            const field = this.metaData.fields[fieldName];\n            if (records.every((rec) => isSameValue(rec, field))) {\n                // All the records already have the given value.\n                delete trimmed[fieldName];\n            }\n        }\n\n        return trimmed;\n    }\n\n    /**\n     * Reschedule a task to the given schedule.\n     *\n     * @param {number | number[]} ids\n     * @param {Record<string, any>} schedule\n     * @param {(result: any) => any} [callback]\n     */\n    async reschedule(ids, schedule, callback) {\n        if (!Array.isArray(ids)) {\n            ids = [ids];\n        }\n        const allData = this._scheduleToData(schedule);\n        const data = this.removeRedundantData(allData, ids);\n        const context = this._getRescheduleContext();\n        return this.mutex.exec(async () => {\n            try {\n                const result = await this._reschedule(ids, data, context);\n                if (callback) {\n                    await callback(result);\n                }\n            } finally {\n                this.fetchData();\n            }\n        });\n    }\n\n    async _reschedule(ids, data, context) {\n        return this.orm.write(this.metaData.resModel, ids, data, {\n            context,\n        });\n    }\n\n    toggleHighlightPlannedFilter(ids) {}\n\n    /**\n     * Reschedule masterId or slaveId according to the direction\n     *\n     * @param {\"forward\" | \"backward\"} direction\n     * @param {number} masterId\n     * @param {number} slaveId\n     * @returns {Promise<any>}\n     */\n    async rescheduleAccordingToDependency(\n        direction,\n        masterId,\n        slaveId,\n        rescheduleAccordingToDependencyCallback\n    ) {\n        const {\n            dateStartField,\n            dateStopField,\n            dependencyField,\n            dependencyInvertedField,\n            resModel,\n        } = this.metaData;\n\n        return await this.mutex.exec(async () => {\n            try {\n                const result = await this.orm.call(resModel, \"web_gantt_reschedule\", [\n                    direction,\n                    masterId,\n                    slaveId,\n                    dependencyField,\n                    dependencyInvertedField,\n                    dateStartField,\n                    dateStopField,\n                ]);\n                if (rescheduleAccordingToDependencyCallback) {\n                    await rescheduleAccordingToDependencyCallback(result);\n                }\n            } finally {\n                this.fetchData();\n            }\n        });\n    }\n\n    /**\n     * @param {string} rowId\n     */\n    toggleRow(rowId) {\n        if (this.isClosed(rowId)) {\n            this.closedRows.delete(rowId);\n        } else {\n            this.closedRows.add(rowId);\n        }\n        this.notify();\n    }\n\n    async toggleDisplayMode() {\n        this.displayParams.displayMode =\n            this.displayParams.displayMode === \"dense\" ? \"sparse\" : \"dense\";\n        this.notify();\n    }\n\n    async updatePagerParams({ limit, offset }) {\n        await this.fetchData({ pagerLimit: limit, pagerOffset: offset });\n    }\n\n    //-------------------------------------------------------------------------\n    // Protected\n    //-------------------------------------------------------------------------\n\n    /**\n     * Return a copy of this.metaData or of the last copy, extended with optional\n     * params. This is useful for async methods that need to modify this.metaData,\n     * but it can't be done in place directly for the model to be concurrency\n     * proof (so they work on a copy and commit it at the end).\n     *\n     * @protected\n     * @param {Object} params\n     * @param {DateTime} [params.focusDate]\n     * @param {DateTime} [params.startDate]\n     * @param {DateTime} [params.stopDate]\n     * @param {string[]} [params.groupedBy]\n     * @param {ScaleId} [params.scaleId]\n     * @returns {MetaData}\n     */\n    _buildMetaData(params = {}) {\n        this._nextMetaData = { ...(this._nextMetaData || this.metaData) };\n\n        if (params.groupedBy) {\n            this._nextMetaData.groupedBy = params.groupedBy;\n        }\n        if (params.scaleId) {\n            browser.localStorage.setItem(this._getLocalStorageKey(), params.scaleId);\n            this._nextMetaData.scale = { ...this._nextMetaData.scales[params.scaleId] };\n        }\n        if (params.focusDate) {\n            this._nextMetaData.focusDate = params.focusDate;\n        }\n        if (params.startDate) {\n            this._nextMetaData.startDate = params.startDate;\n        }\n        if (params.stopDate) {\n            this._nextMetaData.stopDate = params.stopDate;\n        }\n        if (params.rangeId) {\n            this._nextMetaData.rangeId = params.rangeId;\n        }\n\n        if (\"pagerLimit\" in params) {\n            this._nextMetaData.pagerLimit = params.pagerLimit;\n        }\n        if (\"pagerOffset\" in params) {\n            this._nextMetaData.pagerOffset = params.pagerOffset;\n        }\n\n        if (\"scaleId\" in params || \"startDate\" in params || \"stopDate\" in params) {\n            // we assume that scale, startDate, and stopDate are already set in this._nextMetaData\n\n            let exchange = false;\n            if (this._nextMetaData.startDate > this._nextMetaData.stopDate) {\n                exchange = true;\n                const temp = this._nextMetaData.startDate;\n                this._nextMetaData.startDate = this._nextMetaData.stopDate;\n                this._nextMetaData.stopDate = temp;\n            }\n            const { interval } = this._nextMetaData.scale;\n\n            const rightLimit = this._nextMetaData.startDate.plus({ year: 10, day: -1 });\n            if (this._nextMetaData.stopDate > rightLimit) {\n                if (exchange) {\n                    this._nextMetaData.startDate = this._nextMetaData.stopDate.minus({\n                        year: 10,\n                        day: -1,\n                    });\n                } else {\n                    this._nextMetaData.stopDate = this._nextMetaData.startDate.plus({\n                        year: 10,\n                        day: -1,\n                    });\n                }\n            }\n            this._nextMetaData.globalStart = firstColumnBefore(\n                this._nextMetaData.startDate,\n                interval\n            );\n            this._nextMetaData.globalStop = firstColumnAfter(\n                this._nextMetaData.stopDate.plus({ day: 1 }),\n                interval\n            );\n\n            if (params.currentFocusDate) {\n                this._nextMetaData.focusDate = params.currentFocusDate;\n                if (this._nextMetaData.focusDate < this._nextMetaData.startDate) {\n                    this._nextMetaData.focusDate = this._nextMetaData.startDate;\n                } else if (this._nextMetaData.stopDate < this._nextMetaData.focusDate) {\n                    this._nextMetaData.focusDate = this._nextMetaData.stopDate;\n                }\n            }\n        }\n\n        return this._nextMetaData;\n    }\n\n    /**\n     * Fetches records to display (and groups if necessary).\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Object} [additionalContext]\n     */\n    async _fetchData(metaData, additionalContext) {\n        const { globalStart, globalStop, groupedBy, pagerLimit, pagerOffset, resModel, scale } =\n            metaData;\n        const context = {\n            ...this.searchParams.context,\n            group_by: groupedBy,\n            ...additionalContext,\n        };\n        const domain = this._getDomain(metaData);\n        const fields = this._getFields(metaData);\n        const specification = {};\n        for (const fieldName of fields) {\n            specification[fieldName] = {};\n            if (metaData.fields[fieldName].type === \"many2one\") {\n                specification[fieldName].fields = { display_name: {} };\n            }\n        }\n\n        const { length, groups, records, progress_bars, unavailabilities } =\n            await this.keepLast.add(\n                this.orm.call(resModel, \"get_gantt_data\", [], {\n                    domain,\n                    groupby: groupedBy,\n                    read_specification: specification,\n                    scale: scale.unit,\n                    start_date: serializeDateTime(globalStart),\n                    stop_date: serializeDateTime(globalStop),\n                    unavailability_fields: this._getUnavailabilityFields(metaData),\n                    progress_bar_fields: this._getProgressBarFields(metaData),\n                    context,\n                    limit: pagerLimit,\n                    offset: pagerOffset,\n                })\n            );\n\n        groups.forEach((g) => (g.fromServer = true));\n\n        const data = { count: length };\n\n        data.records = this._parseServerData(metaData, records);\n        data.rows = this._generateRows(metaData, {\n            groupedBy,\n            groups,\n            parentGroup: [],\n        });\n        data.unavailabilities = this._processUnavailabilities(unavailabilities);\n        data.progressBars = this._processProgressBars(progress_bars);\n\n        await this.keepLast.add(this._fetchDataPostProcess(metaData, data));\n\n        this.data = data;\n        this.metaData = metaData;\n        this._nextMetaData = null;\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Data} data\n     */\n    async _fetchDataPostProcess(metaData, data) {}\n\n    /**\n     * Remove date in groupedBy field\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @param {string[]} groupedBy\n     * @returns {string[]}\n     */\n    _filterDateIngroupedBy(metaData, groupedBy) {\n        return groupedBy.filter((gb) => {\n            const [fieldName] = gb.split(\":\");\n            const { type } = metaData.fields[fieldName];\n            return ![\"date\", \"datetime\"].includes(type);\n        });\n    }\n\n    /**\n     * @protected\n     * @param {number} floatVal\n     * @param {string}\n     */\n    _formatTime(floatVal) {\n        const timeStr = formatFloatTime(floatVal, { noLeadingZeroHour: true });\n        const [hourStr, minuteStr] = timeStr.split(\":\");\n        const hour = parseInt(hourStr, 10);\n        const minute = parseInt(minuteStr, 10);\n        return minute ? _t(\"%(hour)sh%(minute)s\", { hour, minute }) : _t(\"%sh\", hour);\n    }\n\n    /**\n     * Process groups to generate a recursive structure according\n     * to groupedBy fields. Note that there might be empty groups (filled by\n     * read_goup with group_expand) that also need to be processed.\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Object} params\n     * @param {Object[]} params.groups\n     * @param {string[]} params.groupedBy\n     * @param {Object[]} params.parentGroup\n     * @returns {Row[]}\n     */\n    _generateRows(metaData, params) {\n        const groupedBy = params.groupedBy;\n        const groups = params.groups;\n        const groupLevel = metaData.groupedBy.length - groupedBy.length;\n        const parentGroup = params.parentGroup;\n\n        if (!groupedBy.length || !groups.length) {\n            const recordIds = [];\n            for (const g of groups) {\n                recordIds.push(...(g.__record_ids || []));\n            }\n            const part = parentGroup.at(-1);\n            const [[parentGroupedField, value]] = part ? Object.entries(part) : [[]];\n            return [\n                {\n                    groupLevel,\n                    id: JSON.stringify([...parentGroup, {}]),\n                    name: \"\",\n                    recordIds: unique(recordIds),\n                    parentGroupedField,\n                    parentResId: Array.isArray(value) ? value[0] : value,\n                    __extra__: true,\n                },\n            ];\n        }\n\n        /** @type {Row[]} */\n        const rows = [];\n\n        // Some groups might be empty (thanks to expand_groups), so we can't\n        // simply group the data, we need to keep all returned groups\n        const groupedByField = groupedBy[0];\n        const currentLevelGroups = groupBy(groups, (g) => {\n            if (g[groupedByField] === undefined) {\n                // we want to group the groups with undefined values for groupedByField with the ones\n                // with false value for the same field.\n                // we also want to be sure that stringification keeps groupedByField:\n                // JSON.stringify({ key: undefined }) === \"{}\"\n                // see construction of id below.\n                g[groupedByField] = false;\n            }\n            return g[groupedByField];\n        });\n        const { maxField } = metaData.consolidationParams;\n        const consolidate = groupLevel === 0 && groupedByField === maxField;\n        const generateSubRow = maxField ? true : groupedBy.length > 1;\n        for (const key in currentLevelGroups) {\n            const subGroups = currentLevelGroups[key];\n            const value = subGroups[0][groupedByField];\n            const part = {};\n            part[groupedByField] = value;\n            const fakeGroup = [...parentGroup, part];\n            const id = JSON.stringify(fakeGroup);\n            const resId = Array.isArray(value) ? value[0] : value; // not really a resId\n            const fromServer = subGroups.some((g) => g.fromServer);\n            const recordIds = [];\n            for (const g of subGroups) {\n                recordIds.push(...(g.__record_ids || []));\n            }\n            const row = {\n                consolidate,\n                fromServer,\n                groupedBy,\n                groupedByField,\n                groupLevel,\n                id,\n                name: this._getRowName(metaData, groupedByField, value),\n                resId, // not really a resId\n                recordIds: unique(recordIds),\n            };\n            if (generateSubRow) {\n                row.rows = this._generateRows(metaData, {\n                    ...params,\n                    groupedBy: groupedBy.slice(1),\n                    groups: subGroups,\n                    parentGroup: fakeGroup,\n                });\n            }\n            if (resId === false) {\n                rows.unshift(row);\n            } else {\n                rows.push(row);\n            }\n        }\n\n        return rows;\n    }\n\n    /**\n     * Get domain of records to display in the gantt view.\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @returns {any[]}\n     */\n    _getDomain(metaData) {\n        const { dateStartField, dateStopField, globalStart, globalStop } = metaData;\n        const domain = Domain.and([\n            this.searchParams.domain,\n            [\n                \"&\",\n                [\n                    dateStartField,\n                    \"<\",\n                    this.dateStopFieldIsDate(metaData)\n                        ? serializeDate(globalStop)\n                        : serializeDateTime(globalStop),\n                ],\n                [\n                    dateStopField,\n                    this.dateStartFieldIsDate(metaData) ? \">=\" : \">\",\n                    this.dateStartFieldIsDate(metaData)\n                        ? serializeDate(globalStart)\n                        : serializeDateTime(globalStart),\n                ],\n            ],\n        ]);\n        return domain.toList();\n    }\n\n    /**\n     * Format field value to display purpose.\n     *\n     * @protected\n     * @param {any} value\n     * @param {Object} field\n     * @returns {string} formatted field value\n     */\n    _getFieldFormattedValue(value, field) {\n        if (field.type === \"boolean\") {\n            return value ? \"True\" : \"False\";\n        } else if (!value) {\n            return _t(\"Undefined %s\", field.string);\n        } else if (field.type === \"many2many\") {\n            return value[1];\n        }\n        const formatter = registry.category(\"formatters\").get(field.type);\n        return formatter(value, field);\n    }\n\n    /**\n     * Get all the fields needed.\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @returns {string[]}\n     */\n    _getFields(metaData) {\n        const fields = new Set([\n            \"display_name\",\n            metaData.dateStartField,\n            metaData.dateStopField,\n            ...metaData.groupedBy,\n            ...metaData.decorationFields,\n        ]);\n        if (metaData.colorField) {\n            fields.add(metaData.colorField);\n        }\n        if (metaData.consolidationParams.field) {\n            fields.add(metaData.consolidationParams.field);\n        }\n        if (metaData.consolidationParams.excludeField) {\n            fields.add(metaData.consolidationParams.excludeField);\n        }\n        if (metaData.dependencyField) {\n            fields.add(metaData.dependencyField);\n        }\n        if (metaData.progressField) {\n            fields.add(metaData.progressField);\n        }\n        return [...fields];\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {{ groupBy: string[] }} searchParams\n     * @returns {string[]}\n     */\n    _getGroupedBy(metaData, searchParams) {\n        let groupedBy = [...searchParams.groupBy];\n        groupedBy = groupedBy.filter((gb) => {\n            const [fieldName] = gb.split(\".\");\n            const field = metaData.fields[fieldName];\n            return field?.type !== \"properties\";\n        });\n        groupedBy = this._filterDateIngroupedBy(metaData, groupedBy);\n        if (!groupedBy.length) {\n            groupedBy = metaData.defaultGroupBy;\n        }\n        return groupedBy;\n    }\n\n    _getDefaultFocusDate(metaData, searchParams, scaleId) {\n        const { context } = searchParams;\n        let focusDate =\n            \"initialDate\" in context ? deserializeDateTime(context.initialDate) : DateTime.local();\n        focusDate = focusDate.startOf(\"day\");\n        if (metaData.offset) {\n            const { unit } = metaData.scales[scaleId];\n            focusDate = focusDate.plus({ [unit]: metaData.offset });\n        }\n        return focusDate;\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {{ context: Record<string, any> }} searchParams\n     * @returns {{ focusDate: DateTime, scaleId: ScaleId, startDate: DateTime, stopDate: DateTime }}\n     */\n    _getInitialRangeParams(metaData, searchParams) {\n        const { context } = searchParams;\n        const localScaleId = this._getScaleIdFromLocalStorage(metaData);\n        /** @type {ScaleId} */\n        const scaleId = localScaleId || context.default_scale || metaData.defaultScale;\n        const { defaultRange } = metaData.scales[scaleId];\n\n        const rangeId =\n            context.default_range in metaData.ranges\n                ? context.range_type\n                : metaData.defaultRange || \"custom\";\n        let focusDate;\n        if (rangeId in metaData.ranges) {\n            focusDate = this._getDefaultFocusDate(metaData, searchParams, scaleId);\n            return { scaleId, ...getRangeFromDate(rangeId, focusDate) };\n        }\n        let startDate = context.default_start_date && deserializeDate(context.default_start_date);\n        let stopDate = context.default_stop_date && deserializeDate(context.default_stop_date);\n        if (!startDate && !stopDate) {\n            /** @type {DateTime} */\n            focusDate = this._getDefaultFocusDate(metaData, searchParams, scaleId);\n            startDate = firstColumnBefore(focusDate, defaultRange.unit);\n            stopDate = startDate\n                .plus({ [defaultRange.unit]: defaultRange.count })\n                .minus({ day: 1 });\n        } else if (startDate && !stopDate) {\n            const column = firstColumnBefore(startDate, defaultRange.unit);\n            focusDate = startDate;\n            stopDate = column.plus({ [defaultRange.unit]: defaultRange.count }).minus({ day: 1 });\n        } else if (!startDate && stopDate) {\n            const column = firstColumnAfter(stopDate, defaultRange.unit);\n            focusDate = stopDate;\n            startDate = column.minus({ [defaultRange.unit]: defaultRange.count });\n        } else {\n            focusDate = DateTime.local();\n            if (focusDate < startDate) {\n                focusDate = startDate;\n            } else if (focusDate > stopDate) {\n                focusDate = stopDate;\n            }\n        }\n\n        return { focusDate, scaleId, startDate, stopDate, rangeId };\n    }\n\n    _getLocalStorageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n\n    _getProgressBarFields(metaData) {\n        if (metaData.progressBarFields && !this.orm.isSample) {\n            return metaData.progressBarFields.filter(\n                (fieldName) =>\n                    metaData.groupedBy.includes(fieldName) &&\n                    [\"many2many\", \"many2one\"].includes(metaData.fields[fieldName]?.type)\n            );\n        }\n        return [];\n    }\n\n    _getRescheduleContext() {\n        return { ...this.searchParams.context };\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {string} groupedByField\n     * @param {any} value\n     * @returns {string}\n     */\n    _getRowName(metaData, groupedByField, value) {\n        const field = metaData.fields[groupedByField];\n        return this._getFieldFormattedValue(value, field);\n    }\n\n    _getScaleIdFromLocalStorage(metaData) {\n        const { scales } = metaData;\n        const localScaleId = browser.localStorage.getItem(this._getLocalStorageKey());\n        return localScaleId in scales ? localScaleId : null;\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @returns {string[]}\n     */\n    _getUnavailabilityFields(metaData) {\n        if (metaData.displayUnavailability && !this.orm.isSample && metaData.groupedBy.length) {\n            const lastGroupBy = metaData.groupedBy.at(-1);\n            const { type } = metaData.fields[lastGroupBy] || {};\n            if ([\"many2many\", \"many2one\"].includes(type)) {\n                return [lastGroupBy];\n            }\n        }\n        return [];\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Record<string, any>[]} records the server records to parse\n     * @returns {Record<string, any>[]}\n     */\n    _parseServerData(metaData, records) {\n        const { dateStartField, dateStopField, fields, globalStart, globalStop } = metaData;\n        /** @type {Record<string, any>[]} */\n        const parsedRecords = [];\n        for (const record of records) {\n            const parsedRecord = parseServerValues(fields, record);\n            const dateStart = parsedRecord[dateStartField];\n            const dateStop = parsedRecord[dateStopField];\n            if (this.orm.isSample) {\n                // In sample mode, we want enough data to be displayed, so we\n                // swap the dates as the records are randomly generated anyway.\n                if (dateStart > dateStop) {\n                    parsedRecord[dateStartField] = dateStop;\n                    parsedRecord[dateStopField] = dateStart;\n                }\n                // Record could also be outside the displayed range since the\n                // sample server doesn't take the domain into account\n                if (parsedRecord[dateStopField] < globalStart) {\n                    parsedRecord[dateStopField] = globalStart;\n                }\n                if (parsedRecord[dateStartField] > globalStop) {\n                    parsedRecord[dateStartField] = globalStop;\n                }\n                parsedRecords.push(parsedRecord);\n            } else if (dateStart <= dateStop) {\n                parsedRecords.push(parsedRecord);\n            }\n        }\n        return parsedRecords;\n    }\n\n    _processProgressBar(progressBar, warning) {\n        const processedProgressBar = {\n            ...progressBar,\n            value_formatted: this._formatTime(progressBar.value),\n            max_value_formatted: this._formatTime(progressBar.max_value),\n            ratio: progressBar.max_value ? (progressBar.value / progressBar.max_value) * 100 : 0,\n            warning,\n        };\n        if (processedProgressBar?.max_value) {\n            processedProgressBar.ratio_formatted = formatPercentage(\n                processedProgressBar.ratio / 100\n            );\n        }\n        return processedProgressBar;\n    }\n\n    _processProgressBars(progressBars) {\n        const processedProgressBars = {};\n        for (const fieldName in progressBars) {\n            processedProgressBars[fieldName] = {};\n            const progressBarInfo = progressBars[fieldName];\n            for (const [resId, progressBar] of Object.entries(progressBarInfo)) {\n                processedProgressBars[fieldName][resId] = this._processProgressBar(\n                    progressBar,\n                    progressBarInfo.warning\n                );\n            }\n        }\n        return processedProgressBars;\n    }\n\n    _processUnavailabilities(unavailabilities) {\n        const processedUnavailabilities = {};\n        for (const fieldName in unavailabilities) {\n            processedUnavailabilities[fieldName] = {};\n            for (const [resId, resUnavailabilities] of Object.entries(\n                unavailabilities[fieldName]\n            )) {\n                processedUnavailabilities[fieldName][resId] = resUnavailabilities.map((u) => ({\n                    start: deserializeDateTime(u.start),\n                    stop: deserializeDateTime(u.stop),\n                }));\n            }\n        }\n        return processedUnavailabilities;\n    }\n\n    /**\n     * @template {Record<string, any>} T\n     * @param {T} schedule\n     * @returns {Partial<T>}\n     */\n    _scheduleToData(schedule) {\n        const allowedFields = [\n            this.metaData.dateStartField,\n            this.metaData.dateStopField,\n            ...this.metaData.groupedBy,\n        ];\n        return pick(schedule, ...allowedFields);\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { GanttCompiler } from \"./gantt_compiler\";\n\nexport class GanttPopover extends Component {\n    static template = \"web_gantt.GanttPopover\";\n    static components = { ViewButton };\n    static props = [\n        \"title\",\n        \"displayGenericButtons\",\n        \"bodyTemplate?\",\n        \"footerTemplate?\",\n        \"resModel\",\n        \"resId\",\n        \"context\",\n        \"close\",\n        \"reload\",\n        \"buttons\",\n    ];\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n\n        this.templates = { body: \"web_gantt.GanttPopover.default\" };\n        const toCompile = {};\n        const { bodyTemplate, footerTemplate } = this.props;\n        if (bodyTemplate) {\n            toCompile.body = bodyTemplate;\n            if (footerTemplate) {\n                toCompile.footer = footerTemplate;\n            }\n        }\n        Object.assign(\n            this.templates,\n            useViewCompiler(GanttCompiler, toCompile, { recordExpr: \"__record__\" })\n        );\n\n        useViewButtons(this.rootRef, {\n            reload: async () => {\n                await this.props.reload();\n                this.props.close();\n            },\n        });\n    }\n\n    get renderingContext() {\n        return Object.assign({}, this.props.context, {\n            __comp__: this,\n            __record__: { resModel: this.props.resModel, resId: this.props.resId },\n        });\n    }\n\n    async onClick(button) {\n        await button.onClick();\n        this.props.close();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class GanttPopoverInDialog extends Component {\n    static components = { Dialog };\n    static props = [\"close\", \"component\", \"componentProps\", \"dialogTitle\"];\n    static template = \"web_gantt.GanttPopoverInDialog\";\n    get componentProps() {\n        return { ...this.props.componentProps, close: this.props.close };\n    }\n}\n", "import {\n    Component,\n    onWillRender,\n    onWillStart,\n    onWillUpdateProps,\n    reactive,\n    useEffect,\n    useExternalListener,\n    useRef,\n    markup,\n} from \"@odoo/owl\";\nimport { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Domain } from \"@web/core/domain\";\nimport {\n    getStartOfLocalWeek,\n    is24HourFormat,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\nimport { url } from \"@web/core/utils/urls\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { useVirtualGrid } from \"@web/core/virtual_grid_hook\";\nimport { formatFloatTime } from \"@web/views/fields/formatters\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { GanttConnector } from \"./gantt_connector\";\nimport {\n    dateAddFixedOffset,\n    diffColumn,\n    getCellColor,\n    getColorIndex,\n    localEndOf,\n    localStartOf,\n    useGanttConnectorDraggable,\n    useGanttDraggable,\n    useGanttResizable,\n    useGanttSelectable,\n    useGanttUndraggable,\n    useMultiHover,\n} from \"./gantt_helpers\";\nimport { GanttPopover } from \"./gantt_popover\";\nimport { GanttRendererControls } from \"./gantt_renderer_controls\";\nimport { GanttResizeBadge } from \"./gantt_resize_badge\";\nimport { GanttRowProgressBar } from \"./gantt_row_progress_bar\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {`__column__${number}`} ColumnId\n * @typedef {`__connector__${number | \"new\"}`} ConnectorId\n * @typedef {import(\"./gantt_connector\").ConnectorProps} ConnectorProps\n * @typedef {luxon.DateTime} DateTime\n * @typedef {\"copy\" | \"reschedule\"} DragActionMode\n * @typedef {\"drag\" | \"locked\" | \"resize\"} InteractionMode\n * @typedef {`__pill__${number}`} PillId\n * @typedef {import(\"./gantt_model\").RowId} RowId\n *\n * @typedef Column\n * @property {ColumnId} id\n * @property {GridPosition} grid\n * @property {boolean} [isToday]\n * @property {DateTime} start\n * @property {DateTime} stop\n *\n * @typedef GridPosition\n * @property {number | number[]} [row]\n * @property {number | number[]} [column]\n *\n * @typedef Group\n * @property {boolean} break\n * @property {number} col\n * @property {Pill[]} pills\n * @property {number} aggregateValue\n * @property {GridPosition} grid\n *\n * @typedef GanttRendererProps\n * @property {import(\"./gantt_model\").GanttModel} model\n * @property {Document} arch\n * @property {string} class\n * @property {(context: Record<string, any>)} create\n * @property {{ content?: Point }} [scrollPosition]\n * @property {{ el: HTMLDivElement | null }} [contentRef]\n *\n * @typedef HoveredInfo\n * @property {Element | null} connector\n * @property {HTMLElement | null} hoverable\n * @property {HTMLElement | null} pill\n *\n * @typedef Interaction\n * @property {InteractionMode | null} mode\n * @property {DragActionMode} dragAction\n *\n * @typedef Pill\n * @property {PillId} id\n * @property {boolean} disableStartResize\n * @property {boolean} disableStopResize\n * @property {boolean} highlighted\n * @property {number} leftMargin\n * @property {number} level\n * @property {string} name\n * @property {DateTime} startDate\n * @property {DateTime} stopDate\n * @property {GridPosition} grid\n * @property {RelationalRecord} record\n * @property {number} _color\n * @property {number} _progress\n *\n * @typedef Point\n * @property {number} [x]\n * @property {number} [y]\n *\n * @typedef {Record<string, any>} RelationalRecord\n * @property {number | false} id\n *\n * @typedef ResizeBadge\n * @property {Point & { right?: number }} position\n * @property {number} diff\n * @property {string} scale\n *\n * @typedef {import(\"./gantt_model\").Row & {\n *  grid: GridPosition,\n *  pills: Pill[],\n *  cellColors?: Record<string, string>,\n *  thumbnailUrl?: string\n * }} Row\n *\n * @typedef SubColumn\n * @property {ColumnId} columnId\n * @property {boolean} [isToday]\n * @property {DateTime} start\n * @property {DateTime} stop\n */\n\n/** @type {[Omit<InteractionMode, \"drag\"> | DragActionMode, string][]} */\nconst INTERACTION_CLASSNAMES = [\n    [\"connect\", \"o_connect\"],\n    [\"copy\", \"o_copying\"],\n    [\"locked\", \"o_grabbing_locked\"],\n    [\"reschedule\", \"o_grabbing\"],\n    [\"resize\", \"o_resizing\"],\n];\nconst NEW_CONNECTOR_ID = \"__connector__new\";\n\n/**\n * Gantt Renderer\n *\n * @extends {Component<GanttRendererProps, any>}\n */\nexport class GanttRenderer extends Component {\n    static components = {\n        GanttConnector,\n        GanttRendererControls,\n        GanttResizeBadge,\n        GanttRowProgressBar,\n        Popover: GanttPopover,\n    };\n    static props = [\n        \"model\",\n        \"arch\",\n        \"class\",\n        \"create\",\n        \"openDialog\",\n        \"scrollPosition?\",\n        \"contentRef?\",\n    ];\n\n    static template = \"web_gantt.GanttRenderer\";\n    static connectorCreatorTemplate = \"web_gantt.GanttRenderer.ConnectorCreator\";\n    static headerTemplate = \"web_gantt.GanttRenderer.Header\";\n    static pillTemplate = \"web_gantt.GanttRenderer.Pill\";\n    static groupPillTemplate = \"web_gantt.GanttRenderer.GroupPill\";\n    static rowContentTemplate = \"web_gantt.GanttRenderer.RowContent\";\n    static rowHeaderTemplate = \"web_gantt.GanttRenderer.RowHeader\";\n    static totalRowTemplate = \"web_gantt.GanttRenderer.TotalRow\";\n\n    static getRowHeaderWidth = (width) => 100 / (width > 768 ? 6 : 3);\n\n    setup() {\n        this.model = this.props.model;\n\n        this.gridRef = useRef(\"grid\");\n        this.cellContainerRef = useRef(\"cellContainer\");\n\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.notificationService = useService(\"notification\");\n\n        this.is24HourFormat = is24HourFormat();\n\n        /** @type {HoveredInfo} */\n        this.hovered = {\n            connector: null,\n            hoverable: null,\n            pill: null,\n        };\n\n        /** @type {Interaction} */\n        this.interaction = reactive(\n            {\n                mode: null,\n                dragAction: \"reschedule\",\n            },\n            () => this.onInteractionChange()\n        );\n        this.onInteractionChange(); // Used to hook into \"interaction\"\n        /** @type {Record<ConnectorId, ConnectorProps>} */\n        this.connectors = reactive({});\n        this.progressBarsReactive = reactive({ hoveredRowId: null });\n        /** @type {ResizeBadge} */\n        this.resizeBadgeReactive = reactive({});\n\n        /** @type {Object[]} */\n        this.columnsGroups = [];\n        /** @type {Column[]} */\n        this.columns = [];\n        /** @type {Pill[]} */\n        this.extraPills = [];\n        /** @type {Record<PillId, Pill>} */\n        this.pills = {}; // mapping to retrieve pills from pill ids\n        /** @type {Row[]} */\n        this.rows = [];\n        /** @type {SubColumn[]} */\n        this.subColumns = [];\n        /** @type {Record<RowId, Pill[]>} */\n        this.rowPills = {};\n\n        this.mappingColToColumn = new Map();\n        this.mappingColToSubColumn = new Map();\n        this.cursorPosition = {\n            x: 0,\n            y: 0,\n        };\n        const position = \"bottom\";\n        this.popover = usePopover(this.constructor.components.Popover, {\n            position,\n            onPositioned: (el, { direction }) => {\n                if (direction !== position) {\n                    return;\n                }\n                const { left, right } = el.getBoundingClientRect();\n                if ((0 <= left && right <= window.innerWidth) || window.innerWidth < right - left) {\n                    return;\n                }\n                const { left: pillLeft, right: pillRight } =\n                    this.popover.target.getBoundingClientRect();\n                const middle =\n                    (clamp(pillLeft, 0, window.innerWidth) +\n                        clamp(pillRight, 0, window.innerWidth)) /\n                    2;\n                el.style.left = `0px`;\n                const { width } = el.getBoundingClientRect();\n                el.style.left = `${middle - width / 2}px`;\n            },\n            onClose: () => {\n                delete this.popover.target;\n            },\n        });\n\n        this.throttledComputeHoverParams = throttleForAnimation((ev) =>\n            this.computeHoverParams(ev)\n        );\n\n        useExternalListener(window, \"keydown\", (ev) => this.onWindowKeyDown(ev));\n        useExternalListener(window, \"keyup\", (ev) => this.onWindowKeyUp(ev));\n\n        useExternalListener(\n            window,\n            \"resize\",\n            debounce(() => {\n                this.shouldComputeSomeWidths = true;\n                this.render();\n            }, 100)\n        );\n\n        useMultiHover({\n            ref: this.gridRef,\n            selector: \".o_gantt_group\",\n            related: [\"data-row-id\"],\n            className: \"o_gantt_group_hovered\",\n        });\n\n        // Draggable pills\n        this.cellForDrag = { el: null, part: 0 };\n        const dragState = useGanttDraggable({\n            enable: () => Boolean(this.cellForDrag.el),\n            // Refs and selectors\n            ref: this.gridRef,\n            hoveredCell: this.cellForDrag,\n            elements: \".o_draggable\",\n            ignore: \".o_resize_handle,.o_connector_creator_bullet\",\n            cells: \".o_gantt_cell\",\n            // Style classes\n            cellDragClassName: \"o_gantt_cell o_drag_hover\",\n            ghostClassName: \"o_dragged_pill_ghost\",\n            addStickyCoordinates: (rows, columns) => {\n                this.stickyGridRows = Object.assign({}, ...rows.map((row) => ({ [row]: true })));\n                this.stickyGridColumns = Object.assign(\n                    {},\n                    ...columns.map((column) => ({ [column]: true }))\n                );\n                this.setSomeGridStyleProperties();\n            },\n            // Handlers\n            onDragStart: ({ pill }) => {\n                this.popover.close();\n                this.setStickyPill(pill);\n                this.interaction.mode = \"drag\";\n            },\n            onDragEnd: () => {\n                this.setStickyPill();\n                this.interaction.mode = null;\n            },\n            onDrop: (params) => this.dragPillDrop(params),\n        });\n\n        // Un-draggable pills\n        const unDragState = useGanttUndraggable({\n            // Refs and selectors\n            ref: this.gridRef,\n            elements: \".o_undraggable\",\n            ignore: \".o_resize_handle,.o_connector_creator_bullet\",\n            edgeScrolling: { enabled: false },\n            // Handlers\n            onDragStart: () => {\n                this.interaction.mode = \"locked\";\n            },\n            onDragEnd: () => {\n                this.interaction.mode = null;\n            },\n        });\n\n        // Cells selection\n        const selectState = useGanttSelectable({\n            enable: () => {\n                const { canCellCreate, canPlan } = this.model.metaData;\n                return Boolean(this.cellForDrag.el) && (canCellCreate || canPlan);\n            },\n            ref: this.gridRef,\n            hoveredCell: this.cellForDrag,\n            elements: \".o_gantt_cell:not(.o_gantt_group)\",\n            edgeScrolling: { speed: 40, threshold: 150, direction: \"horizontal\" },\n            rtl: () => localization.direction === \"rtl\",\n            onDrop: ({ rowId, startCol, stopCol }) => {\n                const { canPlan } = this.model.metaData;\n                if (canPlan) {\n                    this.onPlan(rowId, startCol, stopCol);\n                } else {\n                    this.onCreate(rowId, startCol, stopCol);\n                }\n            },\n        });\n\n        // Resizable pills\n        const resizeState = useGanttResizable({\n            // Refs and selectors\n            ref: this.gridRef,\n            hoveredCell: this.cellForDrag,\n            elements: \".o_resizable\",\n            innerPills: \".o_gantt_pill\",\n            cells: \".o_gantt_cell\",\n            // Other params\n            handles: \"o_resize_handle\",\n            edgeScrolling: { speed: 40, threshold: 150, direction: \"horizontal\" },\n            showHandles: (pillEl) => {\n                const pill = this.pills[pillEl.dataset.pillId];\n                const hideHandles = this.connectorDragState.dragging;\n                return {\n                    start: !pill.disableStartResize && !hideHandles,\n                    end: !pill.disableStopResize && !hideHandles,\n                };\n            },\n            rtl: () => localization.direction === \"rtl\",\n            precision: () => this.model.metaData.scale.cellPart,\n            // Handlers\n            onDragStart: ({ pill, addClass }) => {\n                this.popover.close();\n                this.setStickyPill(pill);\n                addClass(pill, \"o_resized\");\n                this.interaction.mode = \"resize\";\n            },\n            onDrag: ({ pill, grabbedHandle, diff }) => {\n                const rect = pill.getBoundingClientRect();\n                const position = { top: rect.y + rect.height };\n                if (grabbedHandle === \"left\") {\n                    position.left = rect.x;\n                } else {\n                    position.right = document.body.offsetWidth - rect.x - rect.width;\n                }\n                const { cellTime, unitDescription } = this.model.metaData.scale;\n                Object.assign(this.resizeBadgeReactive, {\n                    position,\n                    diff: diff * cellTime,\n                    scale: unitDescription,\n                });\n            },\n            onDragEnd: ({ pill, removeClass }) => {\n                delete this.resizeBadgeReactive.position;\n                delete this.resizeBadgeReactive.diff;\n                delete this.resizeBadgeReactive.scale;\n                this.setStickyPill();\n                removeClass(pill, \"o_resized\");\n                this.interaction.mode = null;\n            },\n            onDrop: (params) => this.resizePillDrop(params),\n        });\n\n        // Draggable connector\n        let initialPillId;\n        this.connectorDragState = useGanttConnectorDraggable({\n            ref: this.gridRef,\n            elements: \".o_connector_creator_bullet\",\n            parentWrapper: \".o_gantt_cells .o_gantt_pill_wrapper\",\n            onDragStart: ({ sourcePill, x, y, addClass }) => {\n                this.popover.close();\n                initialPillId = sourcePill.dataset.pillId;\n                addClass(sourcePill, \"o_connector_creator_lock\");\n                this.setConnector({\n                    id: NEW_CONNECTOR_ID,\n                    highlighted: true,\n                    sourcePoint: { left: x, top: y },\n                    targetPoint: { left: x, top: y },\n                });\n                this.setStickyPill(sourcePill);\n                this.interaction.mode = \"connect\";\n            },\n            onDrag: ({ connectorCenter, x, y }) => {\n                this.setConnector({\n                    id: NEW_CONNECTOR_ID,\n                    sourcePoint: { left: connectorCenter.x, top: connectorCenter.y },\n                    targetPoint: { left: x, top: y },\n                });\n            },\n            onDragEnd: () => {\n                this.setConnector({ id: NEW_CONNECTOR_ID, sourcePoint: null, targetPoint: null });\n                this.setStickyPill();\n                this.interaction.mode = null;\n            },\n            onDrop: ({ target }) => {\n                if (initialPillId === target.dataset.pillId) {\n                    return;\n                }\n                const { id: masterId } = this.pills[initialPillId].record;\n                const { id: slaveId } = this.pills[target.dataset.pillId].record;\n                this.model.createDependency(masterId, slaveId);\n            },\n        });\n\n        this.dragStates = [dragState, unDragState, resizeState, selectState];\n\n        onWillStart(this.computeDerivedParams);\n        onWillUpdateProps(this.computeDerivedParams);\n\n        this.virtualGrid = useVirtualGrid({\n            scrollableRef: this.props.contentRef,\n            initialScroll: this.props.scrollPosition,\n            bufferCoef: 0.1,\n            onChange: (changed) => {\n                if (\"columnsIndexes\" in changed) {\n                    this.shouldComputeGridColumns = true;\n                }\n                if (\"rowsIndexes\" in changed) {\n                    this.shouldComputeGridRows = true;\n                }\n                this.render();\n            },\n        });\n\n        onWillRender(this.onWillRender);\n\n        useEffect(\n            (content) => {\n                content.addEventListener(\"scroll\", this.throttledComputeHoverParams);\n                return () => {\n                    content.removeEventListener(\"scroll\", this.throttledComputeHoverParams);\n                };\n            },\n            () => [this.gridRef.el?.parentElement]\n        );\n\n        useEffect(() => {\n            if (this.useFocusDate) {\n                this.useFocusDate = false;\n                this.focusDate(this.model.metaData.focusDate);\n            }\n        });\n\n        this.env.getCurrentFocusDateCallBackRecorder.add(this, this.getCurrentFocusDate.bind(this));\n    }\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    get controlsProps() {\n        return {\n            displayExpandCollapseButtons: this.rows[0]?.isGroup, // all rows on same level have same type\n            model: this.model,\n            focusToday: () => this.focusToday(),\n            getCurrentFocusDate: () => this.getCurrentFocusDate(),\n        };\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get hasRowHeaders() {\n        const { groupedBy } = this.model.metaData;\n        const { displayMode } = this.model.displayParams;\n        return groupedBy.length || displayMode === \"sparse\";\n    }\n\n    get isDragging() {\n        return this.dragStates.some((s) => s.dragging);\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get isTouchDevice() {\n        return isMobileOS() || hasTouch();\n    }\n\n    //-------------------------------------------------------------------------\n    // Methods\n    //-------------------------------------------------------------------------\n\n    /**\n     *\n     * @param {Object} param\n     * @param {Object} param.grid\n     */\n    addCoordinatesToCoarseGrid({ grid }) {\n        if (grid.row) {\n            this.coarseGridRows[this.getFirstGridRow({ grid })] = true;\n            this.coarseGridRows[this.getLastGridRow({ grid })] = true;\n        }\n        if (grid.column) {\n            this.coarseGridCols[this.getFirstGridCol({ grid })] = true;\n            this.coarseGridCols[this.getLastGridCol({ grid })] = true;\n        }\n    }\n\n    /**\n     * @param {Pill} pill\n     * @param {Group} group\n     */\n    addTo(pill, group) {\n        group.pills.push(pill);\n        group.aggregateValue++; // pill count\n        return true;\n    }\n\n    /**\n     * Conditional function for aggregating pills when grouping the gantt view\n     * The first, unused parameter is added in case it's needed when overwriting the method.\n     * @param {Row} row\n     * @param {Group} group\n     * @returns {boolean}\n     */\n    shouldAggregate(row, group) {\n        return Boolean(group.pills.length);\n    }\n\n    /**\n     * Aggregates overlapping pills in group rows.\n     *\n     * @param {Pill[]} pills\n     * @param {Row} row\n     */\n    aggregatePills(pills, row) {\n        /** @type {Record<number, Group>} */\n        const groups = {};\n        function getGroup(col) {\n            if (!(col in groups)) {\n                groups[col] = {\n                    break: false,\n                    col,\n                    pills: [],\n                    aggregateValue: 0,\n                    grid: { column: [col, col + 1] },\n                };\n                // group.break = true means that the group cannot be merged with the previous one\n                // We will merge groups that can be merged together (if this.shouldMergeGroups returns true)\n            }\n            return groups[col];\n        }\n\n        const lastCol = this.columnCount * this.model.metaData.scale.cellPart + 1;\n        for (const pill of pills) {\n            let addedInPreviousCol = false;\n            let col;\n            for (col = this.getFirstGridCol(pill); col < this.getLastGridCol(pill); col++) {\n                const group = getGroup(col);\n                const added = this.addTo(pill, group);\n                if (addedInPreviousCol !== added) {\n                    group.break = true;\n                }\n                addedInPreviousCol = added;\n            }\n            // here col = this.getLastGridCol(pill)\n            if (addedInPreviousCol && col < lastCol) {\n                const group = getGroup(col);\n                group.break = true;\n            }\n        }\n\n        const filteredGroups = Object.values(groups).filter((g) => this.shouldAggregate(row, g));\n\n        if (this.shouldMergeGroups()) {\n            return this.mergeGroups(filteredGroups);\n        }\n\n        return filteredGroups;\n    }\n\n    /**\n     * Compute minimal levels required to display all pills without overlapping.\n     * Side effect: level key is modified in pills.\n     *\n     * @param {Pill[]} pills\n     */\n    calculatePillsLevel(pills) {\n        const firstPill = pills[0];\n        firstPill.level = 0;\n        const levels = [\n            {\n                pills: [firstPill],\n                maxCol: this.getLastGridCol(firstPill) - 1,\n            },\n        ];\n        for (const currentPill of pills.slice(1)) {\n            const lastCol = this.getLastGridCol(currentPill) - 1;\n            for (let l = 0; l < levels.length; l++) {\n                const level = levels[l];\n                if (this.getFirstGridCol(currentPill) > level.maxCol) {\n                    currentPill.level = l;\n                    level.pills.push(currentPill);\n                    level.maxCol = lastCol;\n                    break;\n                }\n            }\n            if (isNaN(currentPill.level)) {\n                currentPill.level = levels.length;\n                levels.push({\n                    pills: [currentPill],\n                    maxCol: lastCol,\n                });\n            }\n        }\n        return levels.length;\n    }\n\n    makeSubColumn(start, delta, cellTime, time) {\n        const subCellStart = dateAddFixedOffset(start, { [time]: delta * cellTime });\n        const subCellStop = dateAddFixedOffset(start, {\n            [time]: (delta + 1) * cellTime,\n            seconds: -1,\n        });\n        return { start: subCellStart, stop: subCellStop };\n    }\n\n    computeVisibleColumns() {\n        const [firstIndex, lastIndex] = this.virtualGrid.columnsIndexes;\n        this.columnsGroups = [];\n        this.columns = [];\n        this.subColumns = [];\n        this.coarseGridCols = {\n            1: true,\n            [this.columnCount * this.model.metaData.scale.cellPart + 1]: true,\n        };\n\n        const { globalStart, globalStop, scale } = this.model.metaData;\n        const { cellPart, interval, unit } = scale;\n\n        const now = DateTime.local();\n\n        const nowStart = now.startOf(interval);\n        const nowEnd = now.endOf(interval);\n\n        const groupsLeftBound = DateTime.max(\n            globalStart,\n            localStartOf(globalStart.plus({ [interval]: firstIndex }), unit)\n        );\n        const groupsRightBound = DateTime.min(\n            localEndOf(globalStart.plus({ [interval]: lastIndex }), unit),\n            globalStop\n        );\n        let currentGroup = null;\n        for (let j = firstIndex; j <= lastIndex; j++) {\n            const columnId = `__column__${j + 1}`;\n            const col = j * cellPart + 1;\n            const { start, stop } = this.getColumnFromColNumber(col);\n            const column = {\n                id: columnId,\n                grid: { column: [col, col + cellPart] },\n                start,\n                stop,\n            };\n            const isToday = nowStart <= start && start <= nowEnd;\n            if (isToday) {\n                column.isToday = true;\n            }\n            this.columns.push(column);\n\n            for (let i = 0; i < cellPart; i++) {\n                const subColumn = this.getSubColumnFromColNumber(col + i);\n                this.subColumns.push({ ...subColumn, isToday, columnId });\n                this.coarseGridCols[col + i] = true;\n            }\n\n            const groupStart = localStartOf(start, unit);\n            if (!currentGroup || !groupStart.equals(currentGroup.start)) {\n                const groupId = `__group__${this.columnsGroups.length + 1}`;\n                const startingBound = DateTime.max(groupsLeftBound, groupStart);\n                const endingBound = DateTime.min(groupsRightBound, localEndOf(groupStart, unit));\n                const [groupFirstCol, groupLastCol] = this.getGridColumnFromDates(\n                    startingBound,\n                    endingBound\n                );\n                currentGroup = {\n                    id: groupId,\n                    grid: { column: [groupFirstCol, groupLastCol] },\n                    start: groupStart,\n                };\n                this.columnsGroups.push(currentGroup);\n                this.coarseGridCols[groupFirstCol] = true;\n                this.coarseGridCols[groupLastCol] = true;\n            }\n        }\n    }\n\n    computeVisibleRows() {\n        this.coarseGridRows = {\n            1: true,\n            [this.getLastGridRow(this.rows[this.rows.length - 1])]: true,\n        };\n        const [rowStart, rowEnd] = this.virtualGrid.rowsIndexes;\n        this.rowsToRender = new Set();\n        for (const row of this.rows) {\n            const [first, last] = row.grid.row;\n            if (last <= rowStart + 1 || first > rowEnd + 1) {\n                continue;\n            }\n            this.addToRowsToRender(row);\n        }\n    }\n\n    getFirstGridCol({ grid }) {\n        const [first] = grid.column;\n        return first;\n    }\n\n    getLastGridCol({ grid }) {\n        const [, last] = grid.column;\n        return last;\n    }\n\n    getFirstGridRow({ grid }) {\n        const [first] = grid.row;\n        return first;\n    }\n\n    getLastGridRow({ grid }) {\n        const [, last] = grid.row;\n        return last;\n    }\n\n    addToPillsToRender(pill) {\n        this.pillsToRender.add(pill);\n        this.addCoordinatesToCoarseGrid(pill);\n    }\n\n    addToRowsToRender(row) {\n        this.rowsToRender.add(row);\n        const [first, last] = row.grid.row;\n        for (let i = first; i <= last; i++) {\n            this.coarseGridRows[i] = true;\n        }\n    }\n\n    /**\n     * give bounds only\n     */\n    getVisibleCols() {\n        const [columnStart, columnEnd] = this.virtualGrid.columnsIndexes;\n        const { cellPart } = this.model.metaData.scale;\n        const firstVisibleCol = 1 + cellPart * columnStart;\n        const lastVisibleCol = 1 + cellPart * (columnEnd + 1);\n        return [firstVisibleCol, lastVisibleCol];\n    }\n\n    /**\n     * give bounds only\n     */\n    getVisibleRows() {\n        const [rowStart, rowEnd] = this.virtualGrid.rowsIndexes;\n        const firstVisibleRow = rowStart + 1;\n        const lastVisibleRow = rowEnd + 1;\n        return [firstVisibleRow, lastVisibleRow];\n    }\n\n    computeVisiblePills() {\n        this.pillsToRender = new Set();\n\n        const [firstVisibleCol, lastVisibleCol] = this.getVisibleCols();\n        const [firstVisibleRow, lastVisibleRow] = this.getVisibleRows();\n\n        const isOut = (pill, filterOnRow = true) =>\n            this.getFirstGridCol(pill) > lastVisibleCol ||\n            this.getLastGridCol(pill) < firstVisibleCol ||\n            (filterOnRow &&\n                (this.getFirstGridRow(pill) > lastVisibleRow ||\n                    this.getLastGridRow(pill) - 1 < firstVisibleRow));\n\n        const getRowPills = (row, filterOnRow) =>\n            (this.rowPills[row.id] || []).filter((pill) => !isOut(pill, filterOnRow));\n\n        for (const row of this.rowsToRender) {\n            for (const rowPill of getRowPills(row)) {\n                this.addToPillsToRender(rowPill);\n            }\n            if (!row.isGroup && row.unavailabilities?.length) {\n                row.cellColors = this.getRowCellColors(row);\n            }\n        }\n\n        if (this.stickyPillId) {\n            this.addToPillsToRender(this.pills[this.stickyPillId]);\n        }\n\n        if (this.totalRow) {\n            this.totalRow.pills = getRowPills(this.totalRow, false);\n            for (const pill of this.totalRow.pills) {\n                this.addCoordinatesToCoarseGrid({ grid: omit(pill.grid, \"row\") });\n            }\n        }\n    }\n\n    computeVisibleConnectors() {\n        const visibleConnectorIds = new Set([NEW_CONNECTOR_ID]);\n\n        for (const pill of this.pillsToRender) {\n            const row = this.getRowFromPill(pill);\n            if (row.isGroup) {\n                continue;\n            }\n            for (const connectorId of this.mappingPillToConnectors[pill.id] || []) {\n                visibleConnectorIds.add(connectorId);\n            }\n        }\n\n        this.connectorsToRender = [];\n        for (const connectorId in this.connectors) {\n            if (!visibleConnectorIds.has(connectorId)) {\n                continue;\n            }\n            this.connectorsToRender.push(this.connectors[connectorId]);\n            const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];\n            if (sourcePillId) {\n                this.addToPillsToRender(this.pills[sourcePillId]);\n            }\n            if (targetPillId) {\n                this.addToPillsToRender(this.pills[targetPillId]);\n            }\n        }\n    }\n\n    getRowFromPill(pill) {\n        return this.rowByIds[pill.rowId];\n    }\n\n    getColInCoarseGridKeys() {\n        return Object.keys({ ...this.coarseGridCols, ...this.stickyGridColumns });\n    }\n\n    getRowInCoarseGridKeys() {\n        return Object.keys({ ...this.coarseGridRows, ...this.stickyGridRows });\n    }\n\n    computeColsTemplate() {\n        const colsTemplate = [];\n        const colInCoarseGridKeys = this.getColInCoarseGridKeys();\n        for (let i = 0; i < colInCoarseGridKeys.length - 1; i++) {\n            const x = +colInCoarseGridKeys[i];\n            const y = +colInCoarseGridKeys[i + 1];\n            const colName = `c${x}`;\n            const width = (y - x) * this.cellPartWidth;\n            colsTemplate.push(`[${colName}]minmax(${width}px,1fr)`);\n        }\n        colsTemplate.push(`[c${colInCoarseGridKeys.at(-1)}]`);\n        return colsTemplate.join(\"\");\n    }\n\n    computeRowsTemplate() {\n        const rowsTemplate = [];\n        const rowInCoarseGridKeys = this.getRowInCoarseGridKeys();\n        for (let i = 0; i < rowInCoarseGridKeys.length - 1; i++) {\n            const x = +rowInCoarseGridKeys[i];\n            const y = +rowInCoarseGridKeys[i + 1];\n            const rowName = `r${x}`;\n            const height = this.gridRows.slice(x - 1, y - 1).reduce((a, b) => a + b, 0);\n            rowsTemplate.push(`[${rowName}]${height}px`);\n        }\n        rowsTemplate.push(`[r${rowInCoarseGridKeys.at(-1)}]`);\n        return rowsTemplate.join(\"\");\n    }\n\n    computeSomeWidths() {\n        const { cellPart, minimalColumnWidth } = this.model.metaData.scale;\n        this.contentRefWidth = this.props.contentRef.el?.clientWidth ?? document.body.clientWidth;\n        const rowHeaderWidthPercentage = this.hasRowHeaders\n            ? this.constructor.getRowHeaderWidth(this.contentRefWidth)\n            : 0;\n        this.rowHeaderWidth = this.hasRowHeaders\n            ? Math.round((rowHeaderWidthPercentage * this.contentRefWidth) / 100)\n            : 0;\n        const cellContainerWidth = this.contentRefWidth - this.rowHeaderWidth;\n        const columnWidth = Math.floor(cellContainerWidth / this.columnCount);\n        const rectifiedColumnWidth = Math.max(columnWidth, minimalColumnWidth);\n        this.cellPartWidth = Math.floor(rectifiedColumnWidth / cellPart);\n        this.columnWidth = this.cellPartWidth * cellPart;\n        if (columnWidth <= minimalColumnWidth) {\n            // overflow\n            this.totalWidth = this.rowHeaderWidth + this.columnWidth * this.columnCount;\n        } else {\n            this.totalWidth = null;\n        }\n    }\n\n    computeDerivedParams() {\n        const { rows: modelRows } = this.model.data;\n\n        if (this.shouldRenderConnectors()) {\n            /** @type {Record<number, { masterIds: number[], pills: Record<RowId, Pill> }>} */\n            this.mappingRecordToPillsByRow = {};\n            /** @type {Record<RowId, Record<number, Pill>>} */\n            this.mappingRowToPillsByRecord = {};\n            /** @type {Record<ConnectorId, { sourcePillId: PillId, targetPillId: PillId }>} */\n            this.mappingConnectorToPills = {};\n            /** @type {Record<PillId, ConnectorId>} */\n            this.mappingPillToConnectors = {};\n        }\n\n        const { globalStart, globalStop, scale, startDate, stopDate } = this.model.metaData;\n        this.columnCount = diffColumn(globalStart, globalStop, scale.interval);\n        if (\n            !this.currentStartDate ||\n            diffColumn(this.currentStartDate, startDate, \"day\") ||\n            diffColumn(this.currentStopDate, stopDate, \"day\") ||\n            this.currentScaleId !== scale.id\n        ) {\n            this.useFocusDate = true;\n            this.mappingColToColumn = new Map();\n            this.mappingColToSubColumn = new Map();\n        }\n        this.currentStartDate = startDate;\n        this.currentStopDate = stopDate;\n        this.currentScaleId = scale.id;\n\n        this.currentGridRow = 1;\n        this.gridRows = [];\n        this.nextPillId = 1;\n\n        this.pills = {}; // mapping to retrieve pills from pill ids\n        this.rows = [];\n        this.rowPills = {};\n        this.rowByIds = {};\n\n        const prePills = this.getPills();\n\n        let pillsToProcess = [...prePills];\n        for (const row of modelRows) {\n            const result = this.processRow(row, pillsToProcess);\n            this.rows.push(...result.rows);\n            pillsToProcess = result.pillsToProcess;\n        }\n\n        const { displayTotalRow } = this.model.metaData;\n        if (displayTotalRow) {\n            this.totalRow = this.getTotalRow(prePills);\n        }\n\n        if (this.shouldRenderConnectors()) {\n            this.initializeConnectors();\n            this.generateConnectors();\n        }\n\n        this.shouldComputeSomeWidths = true;\n        this.shouldComputeGridColumns = true;\n        this.shouldComputeGridRows = true;\n    }\n\n    computeDerivedParamsFromHover() {\n        const { scale } = this.model.metaData;\n\n        const { connector, hoverable, pill } = this.hovered;\n\n        // Update cell in drag\n        const isCellHovered = hoverable?.matches(\".o_gantt_cell\");\n        this.cellForDrag.el = isCellHovered ? hoverable : null;\n        this.cellForDrag.part = 0;\n        if (isCellHovered && scale.cellPart > 1) {\n            const rect = hoverable.getBoundingClientRect();\n            const x = Math.floor(rect.x);\n            const width = Math.floor(rect.width);\n            this.cellForDrag.part = Math.floor(\n                (this.cursorPosition.x - x) / (width / scale.cellPart)\n            );\n            if (localization.direction === \"rtl\") {\n                this.cellForDrag.part = scale.cellPart - 1 - this.cellForDrag.part;\n            }\n        }\n\n        if (this.isDragging) {\n            this.progressBarsReactive.hoveredRowId = null;\n            return;\n        }\n\n        if (!this.connectorDragState.dragging) {\n            // Highlight connector\n            const hoveredConnectorId = connector?.dataset.connectorId;\n            for (const connectorId in this.connectors) {\n                if (connectorId !== hoveredConnectorId) {\n                    this.toggleConnectorHighlighting(connectorId, false);\n                }\n            }\n            if (hoveredConnectorId) {\n                this.progressBarsReactive.hoveredRowId = null;\n                return this.toggleConnectorHighlighting(hoveredConnectorId, true);\n            }\n        }\n\n        // Highlight pill\n        const hoveredPillId = pill?.dataset.pillId;\n        for (const pillId in this.pills) {\n            if (pillId !== hoveredPillId) {\n                this.togglePillHighlighting(pillId, false);\n            }\n        }\n        this.togglePillHighlighting(hoveredPillId, true);\n\n        // Update progress bars\n        this.progressBarsReactive.hoveredRowId = hoverable ? hoverable.dataset.rowId : null;\n    }\n\n    /**\n     * @param {ConnectorId} connectorId\n     */\n    deleteConnector(connectorId) {\n        delete this.connectors[connectorId];\n        delete this.mappingConnectorToPills[connectorId];\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Element} params.pill\n     * @param {Element} params.cell\n     * @param {number} params.diff\n     */\n    async dragPillDrop({ pill, cell, diff }) {\n        const { rowId } = cell.dataset;\n        const { dateStartField, dateStopField, scale } = this.model.metaData;\n        const { cellTime, time } = scale;\n        const { record } = this.pills[pill.dataset.pillId];\n        const params = this.getScheduleParams(pill);\n\n        params.start =\n            diff && dateAddFixedOffset(record[dateStartField], { [time]: cellTime * diff });\n        params.stop =\n            diff && dateAddFixedOffset(record[dateStopField], { [time]: cellTime * diff });\n        params.rowId = rowId;\n\n        const schedule = this.model.getSchedule(params);\n\n        if (this.interaction.dragAction === \"copy\") {\n            await this.model.copy(record.id, schedule, this.openPlanDialogCallback);\n        } else {\n            await this.model.reschedule(record.id, schedule, this.openPlanDialogCallback);\n        }\n\n        // If the pill lands on a closed group -> open it\n        if (cell.classList.contains(\"o_gantt_group\") && this.model.isClosed(rowId)) {\n            this.model.toggleRow(rowId);\n        }\n    }\n\n    /**\n     * @param {Partial<Pill>} pill\n     * @returns {Pill}\n     */\n    enrichPill(pill) {\n        const { colorField, fields, pillDecorations, progressField } = this.model.metaData;\n\n        pill.displayName = this.getDisplayName(pill);\n\n        const classes = [];\n\n        if (pillDecorations) {\n            const pillContext = Object.assign({}, user.context);\n            for (const [fieldName, value] of Object.entries(pill.record)) {\n                const field = fields[fieldName];\n                switch (field.type) {\n                    case \"date\": {\n                        pillContext[fieldName] = value ? serializeDate(value) : false;\n                        break;\n                    }\n                    case \"datetime\": {\n                        pillContext[fieldName] = value ? serializeDateTime(value) : false;\n                        break;\n                    }\n                    default: {\n                        pillContext[fieldName] = value;\n                    }\n                }\n            }\n\n            for (const decoration in pillDecorations) {\n                const expr = pillDecorations[decoration];\n                if (evaluateBooleanExpr(expr, pillContext)) {\n                    classes.push(decoration);\n                }\n            }\n        }\n\n        if (colorField) {\n            pill._color = getColorIndex(pill.record[colorField]);\n            classes.push(`o_gantt_color_${pill._color}`);\n        }\n\n        if (progressField) {\n            pill._progress = pill.record[progressField] || 0;\n        }\n\n        pill.className = classes.join(\" \");\n\n        return pill;\n    }\n\n    focusDate(date, ifInBounds) {\n        const { globalStart, globalStop } = this.model.metaData;\n        const diff = date.diff(globalStart);\n        const totalDiff = globalStop.diff(globalStart);\n        const factor = diff / totalDiff;\n        if (ifInBounds && (factor < 0 || 1 < factor)) {\n            return false;\n        }\n        const rtlFactor = localization.direction === \"rtl\" ? -1 : 1;\n        const scrollLeft =\n            factor * this.cellContainerRef.el.clientWidth +\n            this.rowHeaderWidth -\n            (this.contentRefWidth + this.rowHeaderWidth) / 2;\n        this.props.contentRef.el.scrollLeft = rtlFactor * scrollLeft;\n        return true;\n    }\n\n    focusFirstPill(rowId) {\n        const pill = this.rowPills[rowId][0];\n        if (pill) {\n            const col = this.getFirstGridCol(pill);\n            const { start: date } = this.getColumnFromColNumber(col);\n            this.focusDate(date);\n        }\n    }\n\n    focusToday() {\n        return this.focusDate(DateTime.local().startOf(\"day\"), true);\n    }\n\n    generateConnectors() {\n        this.nextConnectorId = 1;\n        this.setConnector({\n            id: NEW_CONNECTOR_ID,\n            highlighted: true,\n            sourcePoint: null,\n            targetPoint: null,\n        });\n        for (const slaveId in this.mappingRecordToPillsByRow) {\n            const { masterIds, pills: slavePills } = this.mappingRecordToPillsByRow[slaveId];\n            for (const masterId of masterIds) {\n                if (!(masterId in this.mappingRecordToPillsByRow)) {\n                    continue;\n                }\n                const { pills: masterPills } = this.mappingRecordToPillsByRow[masterId];\n                for (const [slaveRowId, targetPill] of Object.entries(slavePills)) {\n                    for (const [masterRowId, sourcePill] of Object.entries(masterPills)) {\n                        if (\n                            masterRowId === slaveRowId ||\n                            !(\n                                slaveId in this.mappingRowToPillsByRecord[masterRowId] ||\n                                masterId in this.mappingRowToPillsByRecord[slaveRowId]\n                            ) ||\n                            Object.keys(this.mappingRecordToPillsByRow[slaveId].pills).every(\n                                (rowId) =>\n                                    rowId !== masterRowId &&\n                                    masterId in this.mappingRowToPillsByRecord[rowId]\n                            ) ||\n                            Object.keys(this.mappingRecordToPillsByRow[masterId].pills).every(\n                                (rowId) =>\n                                    rowId !== slaveRowId &&\n                                    slaveId in this.mappingRowToPillsByRecord[rowId]\n                            )\n                        ) {\n                            const masterRecord = sourcePill.record;\n                            const slaveRecord = targetPill.record;\n                            this.setConnector(\n                                { alert: this.getConnectorAlert(masterRecord, slaveRecord) },\n                                sourcePill.id,\n                                targetPill.id\n                            );\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {Group} group\n     * @param {Group} previousGroup\n     */\n    getAggregateValue(group, previousGroup) {\n        // both groups have the same pills by construction\n        // here the aggregateValue is the pill count\n        return group.aggregateValue;\n    }\n\n    /**\n     * @param {number} startCol\n     * @param {number} stopCol\n     * @param {boolean} [roundUpStop=true]\n     */\n    getColumnStartStop(startCol, stopCol, roundUpStop = true) {\n        const { start } = this.getColumnFromColNumber(startCol);\n        let { stop } = this.getColumnFromColNumber(stopCol);\n        if (roundUpStop) {\n            stop = stop.plus({ millisecond: 1 });\n        }\n        return { start, stop };\n    }\n\n    /**\n     *\n     * @param {number} masterRecord\n     * @param {number} slaveRecord\n     * @returns {import(\"./gantt_connector\").ConnectorAlert | null}\n     */\n    getConnectorAlert(masterRecord, slaveRecord) {\n        const { dateStartField, dateStopField } = this.model.metaData;\n        if (slaveRecord[dateStartField] < masterRecord[dateStopField]) {\n            if (slaveRecord[dateStartField] < masterRecord[dateStartField]) {\n                return \"error\";\n            } else {\n                return \"warning\";\n            }\n        }\n        return null;\n    }\n\n    /**\n     * @param {Row} row\n     * @param {Column} column\n     * @return {Object}\n     */\n    ganttCellAttClass(row, column) {\n        return {\n            o_sample_data_disabled: this.isDisabled(row),\n            o_gantt_today: column.isToday,\n            o_gantt_group: row.isGroup,\n            o_gantt_hoverable: this.isHoverable(row),\n            o_group_open: !this.model.isClosed(row.id),\n        };\n    }\n\n    getCurrentFocusDate() {\n        const { globalStart, globalStop } = this.model.metaData;\n        const rtlFactor = localization.direction === \"rtl\" ? -1 : 1;\n        const cellGridMiddleX =\n            rtlFactor * this.props.contentRef.el.scrollLeft +\n            (this.contentRefWidth + this.rowHeaderWidth) / 2;\n        const factor =\n            (cellGridMiddleX - this.rowHeaderWidth) / this.cellContainerRef.el.clientWidth;\n        const totalDiff = globalStop.diff(globalStart);\n        const diff = factor * totalDiff;\n        const focusDate = globalStart.plus(diff);\n        return focusDate;\n    }\n\n    /**\n     * @param {\"top\"|\"bottom\"} vertical the vertical alignment of the connector creator\n     * @returns {{ vertical: \"top\"|\"bottom\", horizontal: \"left\"|\"right\" }}\n     */\n    getConnectorCreatorAlignment(vertical) {\n        const alignment = { vertical };\n        if (localization.direction === \"rtl\") {\n            alignment.horizontal = vertical === \"top\" ? \"right\" : \"left\";\n        } else {\n            alignment.horizontal = vertical === \"top\" ? \"left\" : \"right\";\n        }\n        return alignment;\n    }\n\n    /**\n     * Get schedule parameters\n     *\n     * @param {Element} pill\n     * @returns {Object} - An object containing parameters needed for scheduling the pill.\n     */\n    getScheduleParams(pill) {\n        return {};\n    }\n\n    /**\n     * This function will add a 'label' property to each\n     * non-consolidated pill included in the pills list.\n     * This new property is a string meant to replace\n     * the text displayed on a pill.\n     *\n     * @param {Pill} pill\n     */\n    getDisplayName(pill) {\n        const { computePillDisplayName, dateStartField, dateStopField, scale } =\n            this.model.metaData;\n        const { id: scaleId } = scale;\n        const { record } = pill;\n\n        if (!computePillDisplayName) {\n            return record.display_name;\n        }\n\n        const startDate = record[dateStartField];\n        const stopDate = record[dateStopField];\n        const yearlessDateFormat = omit(DateTime.DATE_SHORT, \"year\");\n\n        const spanAccrossDays =\n            stopDate.startOf(\"day\") > startDate.startOf(\"day\") &&\n            startDate.endOf(\"day\").diff(startDate, \"hours\").toObject().hours >= 3 &&\n            stopDate.diff(stopDate.startOf(\"day\"), \"hours\").toObject().hours >= 3;\n        const spanAccrossWeeks = getStartOfLocalWeek(stopDate) > getStartOfLocalWeek(startDate);\n        const spanAccrossMonths = stopDate.startOf(\"month\") > startDate.startOf(\"month\");\n\n        /** @type {string[]} */\n        const labelElements = [];\n\n        // Start & End Dates\n        if (scaleId === \"year\" && !spanAccrossDays) {\n            labelElements.push(startDate.toLocaleString(yearlessDateFormat));\n        } else if (\n            (scaleId === \"day\" && spanAccrossDays) ||\n            (scaleId === \"week\" && spanAccrossWeeks) ||\n            (scaleId === \"month\" && spanAccrossMonths) ||\n            (scaleId === \"year\" && spanAccrossDays)\n        ) {\n            labelElements.push(startDate.toLocaleString(yearlessDateFormat));\n            labelElements.push(stopDate.toLocaleString(yearlessDateFormat));\n        }\n\n        // Start & End Times\n        if (record.allocated_hours && !spanAccrossDays && [\"week\", \"month\"].includes(scaleId)) {\n            const durationStr = this.getDurationStr(record);\n            labelElements.push(startDate.toFormat(\"t\"), `${stopDate.toFormat(\"t\")}${durationStr}`);\n        }\n\n        // Original Display Name\n        if (scaleId !== \"month\" || !record.allocated_hours || spanAccrossDays) {\n            labelElements.push(record.display_name);\n        }\n\n        return labelElements.filter((el) => !!el).join(\" - \");\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     */\n    getDurationStr(record) {\n        const durationStr = formatFloatTime(record.allocated_hours, {\n            noLeadingZeroHour: true,\n        }).replace(/(:00|:)/g, \"h\");\n        return ` (${durationStr})`;\n    }\n\n    /**\n     * @param {Pill} pill\n     */\n    getGroupPillDisplayName(pill) {\n        return pill.aggregateValue;\n    }\n\n    /**\n     * @param {{ column?: [number, number], row?: [number, number] }} position\n     */\n    getGridPosition(position) {\n        const style = [];\n        const keys = Object.keys(pick(position, \"column\", \"row\"));\n        for (const key of keys) {\n            const prefix = key.slice(0, 1);\n            const [first, last] = position[key];\n            style.push(`grid-${key}:${prefix}${first}/${prefix}${last}`);\n        }\n        return style.join(\";\");\n    }\n\n    setSomeGridStyleProperties() {\n        const rowsTemplate = this.computeRowsTemplate();\n        const colsTemplate = this.computeColsTemplate();\n        this.gridRef.el.style.setProperty(\"--Gantt__GridRows-grid-template-rows\", rowsTemplate);\n        this.gridRef.el.style.setProperty(\n            \"--Gantt__GridColumns-grid-template-columns\",\n            colsTemplate\n        );\n    }\n\n    getGridStyle() {\n        const rowsTemplate = this.computeRowsTemplate();\n        const colsTemplate = this.computeColsTemplate();\n        const style = {\n            \"--Gantt__RowHeader-width\": `${this.rowHeaderWidth}px`,\n            \"--Gantt__Pill-height\": \"35px\",\n            \"--Gantt__Thumbnail-max-height\": \"16px\",\n            \"--Gantt__GridRows-grid-template-rows\": rowsTemplate,\n            \"--Gantt__GridColumns-grid-template-columns\": colsTemplate,\n        };\n        if (this.totalWidth !== null) {\n            style.width = `${this.totalWidth}px`;\n        }\n        return Object.entries(style)\n            .map((entry) => entry.join(\":\"))\n            .join(\";\");\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @returns {Partial<Pill>}\n     */\n    getPill(record) {\n        const { canEdit, dateStartField, dateStopField, disableDrag, globalStart, globalStop } =\n            this.model.metaData;\n\n        const startOutside = record[dateStartField] < globalStart;\n\n        let recordDateStopField = record[dateStopField];\n        if (this.model.dateStopFieldIsDate()) {\n            recordDateStopField = recordDateStopField.plus({ day: 1 });\n        }\n\n        const stopOutside = recordDateStopField > globalStop;\n\n        /** @type {DateTime} */\n        const pillStartDate = startOutside ? globalStart : record[dateStartField];\n        /** @type {DateTime} */\n        const pillStopDate = stopOutside ? globalStop : recordDateStopField;\n\n        const disableStartResize = !canEdit || startOutside;\n        const disableStopResize = !canEdit || stopOutside;\n\n        /** @type {Partial<Pill>} */\n        const pill = {\n            disableDrag: disableDrag || disableStartResize || disableStopResize,\n            disableStartResize,\n            disableStopResize,\n            grid: { column: this.getGridColumnFromDates(pillStartDate, pillStopDate) },\n            record,\n        };\n\n        return pill;\n    }\n\n    getGridColumnFromDates(startDate, stopDate) {\n        const { globalStart, scale } = this.model.metaData;\n        const { cellPart, interval } = scale;\n        const { column: column1, delta: delta1 } = this.getSubColumnFromDate(startDate);\n        const { column: column2, delta: delta2 } = this.getSubColumnFromDate(stopDate, false);\n        const firstCol = 1 + diffColumn(globalStart, column1, interval) * cellPart + delta1;\n        const span = diffColumn(column1, column2, interval) * cellPart + delta2 - delta1;\n        return [firstCol, firstCol + span];\n    }\n\n    getSubColumnFromDate(date, onLeft = true) {\n        const { interval, cellPart, cellTime, time } = this.model.metaData.scale;\n        const column = date.startOf(interval);\n        let delta;\n        if (onLeft) {\n            delta = 0;\n            for (let i = 1; i < cellPart; i++) {\n                const subCellStart = dateAddFixedOffset(column, { [time]: i * cellTime });\n                if (subCellStart <= date) {\n                    delta += 1;\n                } else {\n                    break;\n                }\n            }\n        } else {\n            delta = cellPart;\n            for (let i = cellPart - 1; i >= 0; i--) {\n                const subCellStart = dateAddFixedOffset(column, { [time]: i * cellTime });\n                if (subCellStart >= date) {\n                    delta -= 1;\n                } else {\n                    break;\n                }\n            }\n        }\n        return { column, delta };\n    }\n\n    getSubColumnFromColNumber(col) {\n        let subColumn = this.mappingColToSubColumn.get(col);\n        if (!subColumn) {\n            const { globalStart, scale } = this.model.metaData;\n            const { interval, cellPart, cellTime, time } = scale;\n            const delta = (col - 1) % cellPart;\n            const columnIndex = (col - 1 - delta) / cellPart;\n            const start = globalStart.plus({ [interval]: columnIndex });\n            subColumn = this.makeSubColumn(start, delta, cellTime, time);\n            this.mappingColToSubColumn.set(col, subColumn);\n        }\n        return subColumn;\n    }\n\n    getColumnFromColNumber(col) {\n        let column = this.mappingColToColumn.get(col);\n        if (!column) {\n            const { globalStart, scale } = this.model.metaData;\n            const { interval, cellPart } = scale;\n            const delta = (col - 1) % cellPart;\n            const columnIndex = (col - 1 - delta) / cellPart;\n            const start = globalStart.plus({ [interval]: columnIndex });\n            const stop = start.endOf(interval);\n            column = { start, stop };\n            this.mappingColToColumn.set(col, column);\n        }\n        return column;\n    }\n\n    /**\n     * @param {PillId} pillId\n     */\n    getPillEl(pillId) {\n        return this.getPillWrapperEl(pillId).querySelector(\".o_gantt_pill\");\n    }\n\n    /**\n     * @param {Object} group\n     * @param {number} maxAggregateValue\n     * @param {boolean} consolidate\n     */\n    getPillFromGroup(group, maxAggregateValue, consolidate) {\n        const { excludeField, field, maxValue } = this.model.metaData.consolidationParams;\n\n        const minColor = 215;\n        const maxColor = 100;\n\n        const newPill = {\n            id: `__pill__${this.nextPillId++}`,\n            level: 0,\n            aggregateValue: group.aggregateValue,\n            grid: group.grid,\n        };\n\n        // Enrich the aggregates with consolidation data\n        if (consolidate && field) {\n            newPill.consolidationValue = 0;\n            for (const pill of group.pills) {\n                if (!pill.record[excludeField]) {\n                    newPill.consolidationValue += pill.record[field];\n                }\n            }\n            newPill.consolidationMaxValue = maxValue;\n            newPill.consolidationExceeded =\n                newPill.consolidationValue > newPill.consolidationMaxValue;\n        }\n\n        if (consolidate && maxValue) {\n            const status = newPill.consolidationExceeded ? \"danger\" : \"success\";\n            newPill.className = `bg-${status} border-${status}`;\n            newPill.displayName = newPill.consolidationValue;\n        } else {\n            const color =\n                minColor -\n                Math.round((newPill.aggregateValue - 1) / maxAggregateValue) *\n                    (minColor - maxColor);\n            newPill.style = `background-color:rgba(${color},${color},${color},0.6)`;\n            newPill.displayName = this.getGroupPillDisplayName(newPill);\n        }\n\n        return newPill;\n    }\n\n    /**\n     * There are two forms of pills: pills comming from fetched records\n     * and pills that are some kind of aggregation of the previous.\n     *\n     * Here we create the pills of the firs type.\n     *\n     * The basic properties (independent of rows,...) of the pills of\n     * the first type should be computed here.\n     *\n     * @returns {Partial<Pill>[]}\n     */\n    getPills() {\n        const { records } = this.model.data;\n        const { dateStartField } = this.model.metaData;\n        const pills = [];\n        for (const record of records) {\n            const pill = this.getPill(record);\n            pills.push(this.enrichPill(pill));\n        }\n        return pills.sort(\n            (p1, p2) =>\n                p1.grid.column[0] - p2.grid.column[0] ||\n                p1.record[dateStartField] - p2.record[dateStartField]\n        );\n    }\n\n    /**\n     * @param {PillId} pillId\n     */\n    getPillWrapperEl(pillId) {\n        const pillSelector = `:scope > [data-pill-id=\"${pillId}\"]`;\n        return this.cellContainerRef.el?.querySelector(pillSelector);\n    }\n\n    /**\n     * Get domain of records for plan dialog in the gantt view.\n     *\n     * @param {Object} state\n     * @returns {any[][]}\n     */\n    getPlanDialogDomain() {\n        const { dateStartField, dateStopField } = this.model.metaData;\n        const newDomain = Domain.removeDomainLeaves(this.env.searchModel.globalDomain, [\n            dateStartField,\n            dateStopField,\n        ]);\n        return Domain.and([\n            newDomain,\n            [\"|\", [dateStartField, \"=\", false], [dateStopField, \"=\", false]],\n        ]).toList({});\n    }\n\n    /**\n     * @param {PillId} pillId\n     * @param {boolean} onRight\n     */\n    getPoint(pillId, onRight) {\n        if (localization.direction === \"rtl\") {\n            onRight = !onRight;\n        }\n        const pillEl = this.getPillEl(pillId);\n        const pillRect = pillEl.getBoundingClientRect();\n        return {\n            left: pillRect.left + (onRight ? pillRect.width : 0),\n            top: pillRect.top + pillRect.height / 2,\n        };\n    }\n\n    /**\n     * @param {Pill} pill\n     */\n    getPopoverProps(pill) {\n        const { record } = pill;\n        const { id: resId, display_name: displayName } = record;\n        const { canEdit, dateStartField, dateStopField, popoverArchParams, resModel } =\n            this.model.metaData;\n        const context = popoverArchParams.bodyTemplate\n            ? { ...record }\n            : /* Default context */ {\n                  name: displayName,\n                  start: record[dateStartField].toFormat(\"f\"),\n                  stop: record[dateStopField].toFormat(\"f\"),\n              };\n\n        return {\n            ...popoverArchParams,\n            title: displayName,\n            context,\n            resId,\n            resModel,\n            reload: () => this.model.fetchData(),\n            buttons: [\n                {\n                    id: \"open_view_edit_dialog\",\n                    text: canEdit ? _t(\"Edit\") : _t(\"View\"),\n                    class: \"btn btn-sm btn-primary\",\n                    // Sync with the mutex to wait for potential changes on the view\n                    onClick: () =>\n                        this.model.mutex.exec(\n                            () => this.props.openDialog({ resId }) // (canEdit is also considered in openDialog)\n                        ),\n                },\n            ],\n        };\n    }\n\n    /**\n     * @param {Row} row\n     */\n    getProgressBarProps(row) {\n        return {\n            progressBar: row.progressBar,\n            reactive: this.progressBarsReactive,\n            rowId: row.id,\n        };\n    }\n\n    /**\n     * @param {Row} row\n     */\n    getRowCellColors(row) {\n        const { unavailabilities } = row;\n        const { cellPart } = this.model.metaData.scale;\n        // We assume that the unavailabilities have been normalized\n        // (i.e. are naturally ordered and are pairwise disjoint).\n        // A subCell is considered unavailable (and greyed) when totally covered by\n        // an unavailability.\n        let index = 0;\n        let j = 0;\n        /** @type {Record<string, string>} */\n        const cellColors = {};\n        const subSlotUnavailabilities = [];\n        for (const subColumn of this.subColumns) {\n            const { isToday, start, stop, columnId } = subColumn;\n            if (index < unavailabilities.length) {\n                let subSlotUnavailable = 0;\n                for (let i = index; i < unavailabilities.length; i++) {\n                    const u = unavailabilities[i];\n                    if (stop > u.stop) {\n                        index++;\n                        continue;\n                    } else if (u.start <= start) {\n                        subSlotUnavailable = 1;\n                    }\n                    break;\n                }\n                subSlotUnavailabilities.push(subSlotUnavailable);\n                if ((j + 1) % cellPart === 0) {\n                    const style = getCellColor(cellPart, subSlotUnavailabilities, isToday);\n                    subSlotUnavailabilities.splice(0, cellPart);\n                    if (style) {\n                        cellColors[columnId] = style;\n                    }\n                }\n                j++;\n            }\n        }\n        return cellColors;\n    }\n\n    getFromData(groupedByField, resId, key, defaultVal) {\n        const values = this.model.data[key];\n        if (groupedByField) {\n            return values[groupedByField]?.[resId ?? false] || defaultVal;\n        }\n        return values.__default?.false || defaultVal;\n    }\n\n    /**\n     * @param {string} [groupedByField]\n     * @param {false|number} [resId]\n     * @returns {Object}\n     */\n    getRowProgressBar(groupedByField, resId) {\n        return this.getFromData(groupedByField, resId, \"progressBars\", null);\n    }\n\n    /**\n     * @param {string} [groupedByField]\n     * @param {false|number} [resId]\n     * @returns {{ start: DateTime, stop: DateTime }[]}\n     */\n    getRowUnavailabilities(groupedByField, resId) {\n        return this.getFromData(groupedByField, resId, \"unavailabilities\", []);\n    }\n\n    /**\n     * @param {\"t0\" | \"t1\" | \"t2\"} type\n     * @returns {number}\n     */\n    getRowTypeHeight(type) {\n        return {\n            t0: 24,\n            t1: 36,\n            t2: 16,\n        }[type];\n    }\n\n    getRowTitleStyle(row) {\n        return `grid-column: ${row.groupLevel + 2} / -1`;\n    }\n\n    openPlanDialogCallback() {}\n\n    getSelectCreateDialogProps(params) {\n        const domain = this.getPlanDialogDomain();\n        const schedule = this.model.getDialogContext(params);\n        return {\n            title: _t(\"Plan\"),\n            resModel: this.model.metaData.resModel,\n            context: schedule,\n            domain,\n            noCreate: !this.model.metaData.canCellCreate,\n            onSelected: (resIds) => {\n                if (resIds.length) {\n                    this.model.reschedule(resIds, schedule, this.openPlanDialogCallback.bind(this));\n                }\n            },\n        };\n    }\n\n    /**\n     * @param {Pill[]} pills\n     */\n    getTotalRow(pills) {\n        const preRow = {\n            groupLevel: 0,\n            id: \"[]\",\n            rows: [],\n            name: _t(\"Total\"),\n            recordIds: pills.map(({ record }) => record.id),\n        };\n\n        this.currentGridRow = 1;\n        const result = this.processRow(preRow, pills);\n        const [totalRow] = result.rows;\n        const allPills = this.rowPills[totalRow.id] || [];\n        const maxAggregateValue = Math.max(...allPills.map((p) => p.aggregateValue));\n\n        totalRow.factor = maxAggregateValue ? 90 / maxAggregateValue : 0;\n\n        return totalRow;\n    }\n\n    highlightPill(pillId, highlighted) {\n        const pill = this.pills[pillId];\n        if (!pill) {\n            return;\n        }\n        pill.highlighted = highlighted;\n        const pillWrapper = this.getPillWrapperEl(pillId);\n        pillWrapper?.classList.toggle(\"highlight\", highlighted);\n        pillWrapper?.classList.toggle(\n            \"o_connector_creator_highlight\",\n            highlighted && this.connectorDragState.dragging\n        );\n    }\n\n    initializeConnectors() {\n        for (const connectorId in this.connectors) {\n            this.deleteConnector(connectorId);\n        }\n    }\n\n    isPillSmall(pill) {\n        return this.cellPartWidth * pill.grid.column[1] < pill.displayName.length * 10;\n    }\n\n    /**\n     * @param {Row} row\n     */\n    isDisabled(row = null) {\n        return this.model.useSampleModel;\n    }\n\n    /**\n     * @param {Row} row\n     */\n    isHoverable(row) {\n        return !this.model.useSampleModel;\n    }\n\n    /**\n     * @param {Group[]} groups\n     * @returns {Group[]}\n     */\n    mergeGroups(groups) {\n        if (groups.length <= 1) {\n            return groups;\n        }\n        const index = Math.floor(groups.length / 2);\n        const left = this.mergeGroups(groups.slice(0, index));\n        const right = this.mergeGroups(groups.slice(index));\n        const group = right[0];\n        if (!group.break) {\n            const previousGroup = left.pop();\n            group.break = previousGroup.break;\n            group.grid.column[0] = previousGroup.grid.column[0];\n            group.aggregateValue = this.getAggregateValue(group, previousGroup);\n        }\n        return [...left, ...right];\n    }\n\n    onWillRender() {\n        if (this.noDisplayedConnectors && this.shouldRenderConnectors()) {\n            delete this.noDisplayedConnectors;\n            this.computeDerivedParams();\n        }\n\n        if (this.shouldComputeSomeWidths) {\n            this.computeSomeWidths();\n        }\n\n        if (this.shouldComputeSomeWidths || this.shouldComputeGridColumns) {\n            this.virtualGrid.setColumnsWidths(new Array(this.columnCount).fill(this.columnWidth));\n            this.computeVisibleColumns();\n        }\n\n        if (this.shouldComputeGridRows) {\n            this.virtualGrid.setRowsHeights(this.gridRows);\n            this.computeVisibleRows();\n        }\n\n        if (\n            this.shouldComputeSomeWidths ||\n            this.shouldComputeGridColumns ||\n            this.shouldComputeGridRows\n        ) {\n            delete this.shouldComputeSomeWidths;\n            delete this.shouldComputeGridColumns;\n            delete this.shouldComputeGridRows;\n            this.computeVisiblePills();\n            if (this.shouldRenderConnectors()) {\n                this.computeVisibleConnectors();\n            } else {\n                this.noDisplayedConnectors = true;\n            }\n        }\n\n        delete this.shouldComputeSomeWidths;\n        delete this.shouldComputeGridColumns;\n        delete this.shouldComputeGridRows;\n    }\n\n    pushGridRows(gridRows) {\n        for (const key of [\"t0\", \"t1\", \"t2\"]) {\n            if (key in gridRows) {\n                const types = new Array(gridRows[key]).fill(this.getRowTypeHeight(key));\n                this.gridRows.push(...types);\n            }\n        }\n    }\n\n    processPillsAsRows(row, pills) {\n        const rows = [];\n        const parsedId = JSON.parse(row.id);\n        if (pills.length) {\n            for (const pill of pills) {\n                const { id: resId, display_name: name } = pill.record;\n                const subRow = {\n                    id: JSON.stringify([...parsedId, { id: resId }]),\n                    resId,\n                    name,\n                    groupLevel: row.groupLevel + 1,\n                    recordIds: [resId],\n                    fromServer: row.fromServer,\n                    parentResId: row.resId ?? row.parentResId,\n                    parentGroupedField: row.groupedByField || row.parentGroupedField,\n                };\n                const res = this.processRow(subRow, [pill], false);\n                rows.push(...res.rows);\n            }\n        } else {\n            const subRow = {\n                id: JSON.stringify([...parsedId, {}]),\n                resId: false,\n                name: \"\",\n                groupLevel: row.groupLevel + 1,\n                recordIds: [],\n                fromServer: row.fromServer,\n                parentResId: row.resId ?? row.parentResId,\n                parentGroupedField: row.groupedByField || row.parentGroupedField,\n            };\n            const res = this.processRow(subRow, [], false);\n            rows.push(...res.rows);\n        }\n\n        return rows;\n    }\n\n    /**\n     * @param {Row} row\n     * @param {Pill[]} pills\n     * @param {boolean} [processAsGroup=false]\n     */\n    processRow(row, pills, processAsGroup = true) {\n        const { dependencyField, displayUnavailability, fields } = this.model.metaData;\n        const { displayMode } = this.model.displayParams;\n        const {\n            consolidate,\n            fromServer,\n            groupedByField,\n            groupLevel,\n            id,\n            name,\n            parentResId,\n            parentGroupedField,\n            resId,\n            rows,\n            recordIds,\n            __extra__,\n        } = row;\n\n        // compute the subset pills at row level\n        const remainingPills = [];\n        let rowPills = [];\n        const groupPills = [];\n        const isMany2many = groupedByField && fields[groupedByField].type === \"many2many\";\n        for (const pill of pills) {\n            const { record } = pill;\n            const pushPill = recordIds.includes(record.id);\n            let keepPill = false;\n            if (pushPill && isMany2many) {\n                const value = record[groupedByField];\n                if (Array.isArray(value) && value.length > 1) {\n                    keepPill = true;\n                }\n            }\n            if (pushPill) {\n                const rowPill = { ...pill };\n                rowPills.push(rowPill);\n                groupPills.push(pill);\n            }\n            if (!pushPill || keepPill) {\n                remainingPills.push(pill);\n            }\n        }\n\n        if (displayMode === \"sparse\" && __extra__) {\n            const rows = this.processPillsAsRows(row, groupPills);\n            return { rows, pillsToProcess: remainingPills };\n        }\n\n        const isGroup = displayMode === \"sparse\" ? processAsGroup : Boolean(rows);\n\n        const gridRowTypes = isGroup ? { t0: 1 } : { t1: 1 };\n        if (rowPills.length) {\n            if (isGroup) {\n                if (this.shouldComputeAggregateValues(row)) {\n                    const groups = this.aggregatePills(rowPills, row);\n                    const maxAggregateValue = Math.max(\n                        ...groups.map((group) => group.aggregateValue)\n                    );\n                    rowPills = groups.map((group) =>\n                        this.getPillFromGroup(group, maxAggregateValue, consolidate)\n                    );\n                } else {\n                    rowPills = [];\n                }\n            } else {\n                const level = this.calculatePillsLevel(rowPills);\n                gridRowTypes.t1 = level;\n                if (!this.isTouchDevice) {\n                    gridRowTypes.t2 = 1;\n                }\n            }\n        }\n\n        const progressBar = this.getRowProgressBar(groupedByField, resId);\n        if (progressBar && this.isTouchDevice && (!gridRowTypes.t1 || gridRowTypes.t1 === 1)) {\n            // In mobile: rows span over 2 rows to alllow progressbars to properly display\n            gridRowTypes.t1 = (gridRowTypes.t1 || 0) + 1;\n        }\n        if (row.id !== \"[]\") {\n            this.pushGridRows(gridRowTypes);\n        }\n\n        for (const rowPill of rowPills) {\n            rowPill.id = `__pill__${this.nextPillId++}`;\n            const pillFirstRow = this.currentGridRow + rowPill.level;\n            rowPill.grid = {\n                ...rowPill.grid, // rowPill is a shallow copy of a prePill (possibly copied several times)\n                row: [pillFirstRow, pillFirstRow + 1],\n            };\n            if (!isGroup) {\n                const { record } = rowPill;\n                if (this.shouldRenderRecordConnectors(record)) {\n                    if (!this.mappingRecordToPillsByRow[record.id]) {\n                        this.mappingRecordToPillsByRow[record.id] = {\n                            masterIds: record[dependencyField],\n                            pills: {},\n                        };\n                    }\n                    this.mappingRecordToPillsByRow[record.id].pills[id] = rowPill;\n                    if (!this.mappingRowToPillsByRecord[id]) {\n                        this.mappingRowToPillsByRecord[id] = {};\n                    }\n                    this.mappingRowToPillsByRecord[id][record.id] = rowPill;\n                }\n            }\n            rowPill.rowId = id;\n            this.pills[rowPill.id] = rowPill;\n        }\n\n        this.rowPills[id] = rowPills; // all row pills\n\n        const subRowsCount = Object.values(gridRowTypes).reduce((acc, val) => acc + val, 0);\n        /** @type {Row} */\n        const processedRow = {\n            cellColors: {},\n            fromServer,\n            groupedByField,\n            groupLevel,\n            id,\n            isGroup,\n            name,\n            progressBar,\n            resId,\n            grid: {\n                row: [this.currentGridRow, this.currentGridRow + subRowsCount],\n            },\n        };\n        if (displayUnavailability && !isGroup) {\n            processedRow.unavailabilities = this.getRowUnavailabilities(\n                parentGroupedField || groupedByField,\n                parentResId ?? resId\n            );\n        }\n\n        this.rowByIds[id] = processedRow;\n\n        this.currentGridRow += subRowsCount;\n\n        const field = this.model.metaData.thumbnails[groupedByField];\n        if (field) {\n            const model = this.model.metaData.fields[groupedByField].relation;\n            processedRow.thumbnailUrl = url(\"/web/image\", {\n                model,\n                id: resId,\n                field,\n            });\n        }\n\n        const result = { rows: [processedRow], pillsToProcess: remainingPills };\n\n        if (!this.model.isClosed(id)) {\n            if (rows) {\n                let pillsToProcess = groupPills;\n                for (const subRow of rows) {\n                    const res = this.processRow(subRow, pillsToProcess);\n                    result.rows.push(...res.rows);\n                    pillsToProcess = res.pillsToProcess;\n                }\n            } else if (displayMode === \"sparse\" && processAsGroup) {\n                const rows = this.processPillsAsRows(row, groupPills);\n                result.rows.push(...rows);\n            }\n        }\n\n        return result;\n    }\n\n    /**\n     * @param {string} [groupedByField]\n     * @param {false|number} [resId]\n     * @returns {{ start: DateTime, stop: DateTime }[]}\n     */\n    _getRowUnavailabilities(groupedByField, resId) {\n        const { unavailabilities } = this.model.data;\n        if (groupedByField) {\n            return unavailabilities[groupedByField]?.[resId ?? false] || [];\n        }\n        return unavailabilities.__default?.false || [];\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Element} params.pill\n     * @param {number} params.diff\n     * @param {\"start\" | \"end\"} params.direction\n     */\n    async resizePillDrop({ pill, diff, direction }) {\n        const { dateStartField, dateStopField, scale } = this.model.metaData;\n        const { cellTime, time } = scale;\n        const { record } = this.pills[pill.dataset.pillId];\n        const params = this.getScheduleParams(pill);\n\n        if (direction === \"start\") {\n            params.start = dateAddFixedOffset(record[dateStartField], { [time]: cellTime * diff });\n            if (params.start > record[dateStopField]) {\n                return this.notificationService.add(\n                    _t(\"Starting date cannot be after the ending date\"),\n                    {\n                        type: \"warning\",\n                    }\n                );\n            }\n        } else {\n            params.stop = dateAddFixedOffset(record[dateStopField], { [time]: cellTime * diff });\n            if (params.stop < record[dateStartField]) {\n                return this.notificationService.add(\n                    _t(\"Ending date cannot be before the starting date\"),\n                    {\n                        type: \"warning\",\n                    }\n                );\n            }\n        }\n        const schedule = this.model.getSchedule(params);\n\n        await this.model.reschedule(record.id, schedule, this.openPlanDialogCallback);\n    }\n\n    /**\n     * @param {Partial<ConnectorProps>} params\n     * @param {PillId | null} [sourceId=null]\n     * @param {PillId | null} [targetId=null]\n     */\n    setConnector(params, sourceId = null, targetId = null) {\n        const connectorParams = { ...params };\n        const connectorId = params.id || `__connector__${this.nextConnectorId++}`;\n\n        if (sourceId) {\n            connectorParams.sourcePoint = () => this.getPoint(sourceId, true);\n        }\n\n        if (targetId) {\n            connectorParams.targetPoint = () => this.getPoint(targetId, false);\n        }\n\n        if (this.connectors[connectorId]) {\n            Object.assign(this.connectors[connectorId], connectorParams);\n        } else {\n            this.connectors[connectorId] = {\n                id: connectorId,\n                highlighted: false,\n                displayButtons: false,\n                ...connectorParams,\n            };\n            this.mappingConnectorToPills[connectorId] = {\n                sourcePillId: sourceId,\n                targetPillId: targetId,\n            };\n        }\n\n        if (sourceId) {\n            if (!this.mappingPillToConnectors[sourceId]) {\n                this.mappingPillToConnectors[sourceId] = [];\n            }\n            this.mappingPillToConnectors[sourceId].push(connectorId);\n        }\n\n        if (targetId) {\n            if (!this.mappingPillToConnectors[targetId]) {\n                this.mappingPillToConnectors[targetId] = [];\n            }\n            this.mappingPillToConnectors[targetId].push(connectorId);\n        }\n    }\n\n    /**\n     * @param {HTMLElement} [pillEl]\n     */\n    setStickyPill(pillEl) {\n        this.stickyPillId = pillEl ? pillEl.dataset.pillId : null;\n    }\n\n    /**\n     * @param {Row} row\n     */\n    shouldComputeAggregateValues(row) {\n        return true;\n    }\n\n    shouldMergeGroups() {\n        return true;\n    }\n\n    /**\n     * Returns whether connectors should be rendered or not.\n     * The connectors won't be rendered on sampleData as we can't be sure that data are coherent.\n     * The connectors won't be rendered on mobile as the usability is not guarantied.\n     *\n     * @return {boolean}\n     */\n    shouldRenderConnectors() {\n        return (\n            this.model.metaData.dependencyField && !this.model.useSampleModel && !this.env.isSmall\n        );\n    }\n\n    /**\n     * Returns whether connectors should be rendered on particular records or not.\n     * This method is intended to be overridden in particular modules in order to set particular record's condition.\n     *\n     * @param {RelationalRecord} record\n     * @return {boolean}\n     */\n    shouldRenderRecordConnectors(record) {\n        return this.shouldRenderConnectors();\n    }\n\n    /**\n     * @param {ConnectorId | null} connectorId\n     * @param {boolean} highlighted\n     */\n    toggleConnectorHighlighting(connectorId, highlighted) {\n        const connector = this.connectors[connectorId];\n        if (!connector || (!connector.highlighted && !highlighted)) {\n            return;\n        }\n\n        connector.highlighted = highlighted;\n        connector.displayButtons = highlighted;\n\n        const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];\n\n        this.highlightPill(sourcePillId, highlighted);\n        this.highlightPill(targetPillId, highlighted);\n    }\n\n    /**\n     * @param {PillId} pillId\n     * @param {boolean} highlighted\n     */\n    togglePillHighlighting(pillId, highlighted) {\n        const pill = this.pills[pillId];\n        if (!pill || pill.highlighted === highlighted) {\n            return;\n        }\n\n        const { record } = pill;\n        const pillIdsToHighlight = new Set([pillId]);\n\n        if (record && this.shouldRenderRecordConnectors(record)) {\n            // Find other related pills\n            const { pills: relatedPills } = this.mappingRecordToPillsByRow[record.id];\n            for (const pill of Object.values(relatedPills)) {\n                pillIdsToHighlight.add(pill.id);\n            }\n\n            // Highlight related connectors\n            for (const [connectorId, connector] of Object.entries(this.connectors)) {\n                const ids = Object.values(this.getRecordIds(connectorId));\n                if (ids.includes(record.id)) {\n                    connector.highlighted = highlighted;\n                    connector.displayButtons = false;\n                }\n            }\n        }\n\n        // Highlight pills from found IDs\n        for (const id of pillIdsToHighlight) {\n            this.highlightPill(id, highlighted);\n        }\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    onCellClicked(rowId, col) {\n        if (!this.preventClick) {\n            this.preventClick = true;\n            setTimeout(() => (this.preventClick = false), 1000);\n            const { canCellCreate, canPlan } = this.model.metaData;\n            if (canPlan) {\n                this.onPlan(rowId, col, col);\n            } else if (canCellCreate) {\n                this.onCreate(rowId, col, col);\n            }\n        }\n    }\n\n    onCreate(rowId, startCol, stopCol) {\n        const { start, stop } = this.getColumnStartStop(startCol, stopCol);\n        const context = this.model.getDialogContext({\n            rowId,\n            start,\n            stop,\n            withDefault: true,\n        });\n        this.props.create(context);\n    }\n\n    onInteractionChange() {\n        let { dragAction, mode } = this.interaction;\n        if (mode === \"drag\") {\n            mode = dragAction;\n        }\n        if (this.gridRef.el) {\n            for (const [action, className] of INTERACTION_CLASSNAMES) {\n                this.gridRef.el.classList.toggle(className, mode === action);\n            }\n        }\n    }\n\n    onPointerLeave() {\n        this.throttledComputeHoverParams.cancel();\n\n        if (!this.isDragging) {\n            const hoveredConnectorId = this.hovered.connector?.dataset.connectorId;\n            this.toggleConnectorHighlighting(hoveredConnectorId, false);\n\n            const hoveredPillId = this.hovered.pill?.dataset.pillId;\n            this.togglePillHighlighting(hoveredPillId, false);\n        }\n\n        this.hovered.connector = null;\n        this.hovered.pill = null;\n        this.hovered.hoverable = null;\n\n        this.computeDerivedParamsFromHover();\n    }\n\n    /**\n     * Updates all hovered elements, then calls \"computeDerivedParamsFromHover\".\n     *\n     * @see computeDerivedParamsFromHover\n     * @param {Event} ev\n     */\n    computeHoverParams(ev) {\n        // Lazily compute elements from point as it is a costly operation\n        let els = null;\n        let position = {};\n        if (ev.type === \"scroll\") {\n            position = this.cursorPosition;\n        } else {\n            position.x = ev.clientX;\n            position.y = ev.clientY;\n            this.cursorPosition = position;\n        }\n        const pointedEls = () => els || (els = document.elementsFromPoint(position.x, position.y));\n\n        // To find hovered elements, also from pointed elements\n        const find = (selector) =>\n            ev.target.closest?.(selector) ||\n            pointedEls().find((el) => el.matches(selector)) ||\n            null;\n\n        this.hovered.connector = find(\".o_gantt_connector\");\n        this.hovered.hoverable = find(\".o_gantt_hoverable\");\n        this.hovered.pill = find(\".o_gantt_pill_wrapper\");\n\n        this.computeDerivedParamsFromHover();\n    }\n\n    /**\n     * @param {PointerEvent} ev\n     * @param {Pill} pill\n     */\n    onPillClicked(ev, pill) {\n        if (this.popover.isOpen) {\n            return;\n        }\n        this.popover.target = ev.target.closest(\".o_gantt_pill_wrapper\");\n        this.popover.open(this.popover.target, this.getPopoverProps(pill));\n    }\n\n    onPlan(rowId, startCol, stopCol) {\n        const { start, stop } = this.getColumnStartStop(startCol, stopCol);\n        this.dialogService.add(\n            SelectCreateDialog,\n            this.getSelectCreateDialogProps({ rowId, start, stop, withDefault: true })\n        );\n    }\n\n    getRecordIds(connectorId) {\n        const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];\n        return {\n            masterId: this.pills[sourcePillId]?.record.id,\n            slaveId: this.pills[targetPillId]?.record.id,\n        };\n    }\n\n    /**\n     *\n     * @param {Object} params\n     * @param {ConnectorId} connectorId\n     */\n    onRemoveButtonClick(connectorId) {\n        const { masterId, slaveId } = this.getRecordIds(connectorId);\n        this.model.removeDependency(masterId, slaveId);\n    }\n    rescheduleAccordingToDependencyCallback(result) {\n        if (result[\"type\"] !== \"warning\" && \"old_vals_per_pill_id\" in result) {\n            this.model.toggleHighlightPlannedFilter(\n                Object.keys(result[\"old_vals_per_pill_id\"]).map(Number)\n            );\n        }\n        this.notificationFn?.();\n        this.notificationFn = this.notificationService.add(\n            markup(\n                `<i class=\"fa btn-link fa-check\"></i><span class=\"ms-1\">${escape(\n                    result[\"message\"]\n                )}</span>`\n            ),\n            {\n                type: result[\"type\"],\n                sticky: true,\n                buttons:\n                    result[\"type\"] === \"warning\"\n                        ? []\n                        : [\n                              {\n                                  name: \"Undo\",\n                                  icon: \"fa-undo\",\n                                  onClick: async () => {\n                                      const ids = Object.keys(result[\"old_vals_per_pill_id\"]).map(\n                                          Number\n                                      );\n                                      await this.orm.call(\n                                          this.model.metaData.resModel,\n                                          \"action_rollback_scheduling\",\n                                          [ids, result[\"old_vals_per_pill_id\"]]\n                                      );\n                                      this.notificationFn();\n                                      await this.model.fetchData();\n                                  },\n                              },\n                          ],\n            }\n        );\n    }\n\n    /**\n     *\n     * @param {\"forward\" | \"backward\"} direction\n     * @param {ConnectorId} connectorId\n     */\n    async onRescheduleButtonClick(direction, connectorId) {\n        const { masterId, slaveId } = this.getRecordIds(connectorId);\n        await this.model.rescheduleAccordingToDependency(\n            direction,\n            masterId,\n            slaveId,\n            this.rescheduleAccordingToDependencyCallback.bind(this)\n        );\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onWindowKeyDown(ev) {\n        if (ev.key === \"Control\") {\n            this.prevDragAction =\n                this.interaction.dragAction === \"copy\" ? \"reschedule\" : this.interaction.dragAction;\n            this.interaction.dragAction = \"copy\";\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onWindowKeyUp(ev) {\n        if (ev.key === \"Control\") {\n            this.interaction.dragAction = this.prevDragAction || \"reschedule\";\n        }\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_hook\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport {\n    diffColumn,\n    getRangeFromDate,\n    localStartOf,\n    useGanttResponsivePopover,\n} from \"./gantt_helpers\";\n\nconst { DateTime } = luxon;\n\nconst KEYS = [\"startDate\", \"stopDate\", \"rangeId\", \"focusDate\"];\n\nexport class GanttRendererControls extends Component {\n    static template = \"web_gantt.GanttRendererControls\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = [\"model\", \"displayExpandCollapseButtons\", \"focusToday\", \"getCurrentFocusDate\"];\n    static toolbarContentTemplate = \"web_gantt.GanttRendererControls.ToolbarContent\";\n    static rangeMenuTemplate = \"web_gantt.GanttRendererControls.RangeMenu\";\n\n    setup() {\n        this.model = this.props.model;\n        this.updateMetaData = debounce(() => this.model.fetchData(this.makeParams()), 500);\n\n        const { metaData } = this.model;\n        this.state = useState({\n            scaleIndex: this.getScaleIndex(metaData.scale.id),\n            ...pick(metaData, ...KEYS),\n        });\n        this.pickerValues = useState({\n            startDate: metaData.startDate,\n            stopDate: metaData.stopDate,\n        });\n        this.scalesRange = { min: 0, max: Object.keys(metaData.scales).length - 1 };\n\n        const getPickerProps = (key) => ({ type: \"date\", value: this.pickerValues[key] });\n        this.startPicker = useDateTimePicker({\n            target: \"start-picker\",\n            onApply: (date) => {\n                this.pickerValues.startDate = date;\n                if (this.pickerValues.stopDate < date) {\n                    this.pickerValues.stopDate = date;\n                } else if (date.plus({ year: 10, day: -1 }) < this.pickerValues.stopDate) {\n                    this.pickerValues.stopDate = date.plus({ year: 10, day: -1 });\n                }\n            },\n            get pickerProps() {\n                return getPickerProps(\"startDate\");\n            },\n            createPopover: (...args) => useGanttResponsivePopover(_t(\"Gantt start date\"), ...args),\n            ensureVisibility: () => false,\n        });\n        this.stopPicker = useDateTimePicker({\n            target: \"stop-picker\",\n            onApply: (date) => {\n                this.pickerValues.stopDate = date;\n                if (date < this.pickerValues.startDate) {\n                    this.pickerValues.startDate = date;\n                } else if (this.pickerValues.startDate.plus({ year: 10, day: -1 }) < date) {\n                    this.pickerValues.startDate = date.minus({ year: 10, day: -1 });\n                }\n            },\n            get pickerProps() {\n                return getPickerProps(\"stopDate\");\n            },\n            createPopover: (...args) => useGanttResponsivePopover(_t(\"Gantt stop date\"), ...args),\n            ensureVisibility: () => false,\n        });\n\n        this.dropdownState = useDropdownState();\n    }\n\n    get dateDescription() {\n        const { focusDate, rangeId } = this.state;\n        switch (rangeId) {\n            case \"quarter\":\n                return focusDate.toFormat(`Qq yyyy`);\n            case \"day\":\n                return formatDate(focusDate);\n            default:\n                return this.model.metaData.ranges[rangeId].groupHeaderFormatter(\n                    focusDate,\n                    this.env\n                );\n        }\n    }\n\n    get formattedDateRange() {\n        return _t(\"From: %(from_date)s to: %(to_date)s\", {\n            from_date: formatDate(this.state.startDate),\n            to_date: formatDate(this.state.stopDate),\n        });\n    }\n\n    getFormattedDate(date) {\n        return formatDate(date);\n    }\n\n    getScaleIdFromIndex(index) {\n        const keys = Object.keys(this.model.metaData.scales);\n        return keys[keys.length - 1 - index];\n    }\n\n    getScaleIndex(scaleId) {\n        const keys = Object.keys(this.model.metaData.scales);\n        return keys.length - 1 - keys.findIndex((id) => id === scaleId);\n    }\n\n    getScaleIndexFromRangeId(rangeId) {\n        const { ranges } = this.model.metaData;\n        const scaleId = ranges[rangeId].scaleId;\n        return this.getScaleIndex(scaleId);\n    }\n\n    /**\n     * @param {1|-1} inc\n     */\n    incrementScale(inc) {\n        if (\n            inc === 1\n                ? this.state.scaleIndex < this.scalesRange.max\n                : this.scalesRange.min < this.state.scaleIndex\n        ) {\n            this.state.scaleIndex += inc;\n            this.updateMetaData();\n        }\n    }\n\n    isSelected(rangeId) {\n        if (rangeId === \"custom\") {\n            return (\n                this.state.rangeId === rangeId ||\n                !localStartOf(this.state.focusDate, this.state.rangeId).equals(\n                    localStartOf(DateTime.now(), this.state.rangeId)\n                )\n            );\n        }\n        return (\n            this.state.rangeId === rangeId &&\n            localStartOf(this.state.focusDate, rangeId).equals(\n                localStartOf(DateTime.now(), rangeId)\n            )\n        );\n    }\n\n    makeParams() {\n        return {\n            currentFocusDate: this.props.getCurrentFocusDate(),\n            scaleId: this.getScaleIdFromIndex(this.state.scaleIndex),\n            ...pick(this.state, ...KEYS),\n        };\n    }\n\n    onApply() {\n        this.state.startDate = this.pickerValues.startDate;\n        this.state.stopDate = this.pickerValues.stopDate;\n        this.state.rangeId = \"custom\";\n        this.updateMetaData();\n        this.dropdownState.close();\n    }\n\n    onTodayClicked() {\n        const success = this.props.focusToday();\n        if (success) {\n            return;\n        }\n        this.state.focusDate = DateTime.local().startOf(\"day\");\n        if (this.state.rangeId === \"custom\") {\n            const diff = diffColumn(this.state.startDate, this.state.stopDate, \"day\");\n            const n = Math.floor(diff / 2);\n            const m = diff - n;\n            this.state.startDate = this.state.focusDate.minus({ day: n });\n            this.state.stopDate = this.state.focusDate.plus({ day: m - 1 });\n        } else {\n            this.state.startDate = this.state.focusDate.startOf(this.state.rangeId);\n            this.state.stopDate = this.state.focusDate.endOf(this.state.rangeId).startOf(\"day\");\n        }\n        this.updatePickerValues();\n        this.updateMetaData();\n    }\n\n    selectRange(direction) {\n        const sign = direction === \"next\" ? 1 : -1;\n        const { focusDate, rangeId, startDate, stopDate } = this.state;\n        if (rangeId === \"custom\") {\n            const diff = diffColumn(startDate, stopDate, \"day\") + 1;\n            this.state.focusDate = focusDate.plus({ day: sign * diff });\n            this.state.startDate = startDate.plus({ day: sign * diff });\n            this.state.stopDate = stopDate.plus({ day: sign * diff });\n        } else {\n            Object.assign(\n                this.state,\n                getRangeFromDate(rangeId, focusDate.plus({ [rangeId]: sign }))\n            );\n        }\n        this.updatePickerValues();\n        this.updateMetaData();\n    }\n\n    selectRangeId(rangeId) {\n        Object.assign(this.state, getRangeFromDate(rangeId, DateTime.now().startOf(\"day\")));\n        this.state.scaleIndex = this.getScaleIndexFromRangeId(rangeId);\n        this.updatePickerValues();\n        this.updateMetaData();\n    }\n\n    selectScale(index) {\n        this.state.scaleIndex = Number(index);\n        this.updateMetaData();\n    }\n\n    updatePickerValues() {\n        this.pickerValues.startDate = this.state.startDate;\n        this.pickerValues.stopDate = this.state.stopDate;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class GanttResizeBadge extends Component {\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                position: {\n                    type: Object,\n                    shape: {\n                        top: Number,\n                        right: { type: Number, optional: true },\n                        left: { type: Number, optional: true },\n                    },\n                    optional: true,\n                },\n                diff: { type: Number, optional: true },\n                scale: { type: String, optional: true },\n            },\n        },\n    };\n    static template = \"web_gantt.GanttResizeBadge\";\n\n    get diff() {\n        return this.props.reactive.diff || 0;\n    }\n\n    get diffText() {\n        const { diff, props } = this;\n        const prefix = this.diff > 0 ? \"+\" : \"\";\n        return `${prefix}${diff} ${props.reactive.scale}`;\n    }\n\n    get positionStyle() {\n        const { position } = this.props.reactive;\n        const style = [`top:${position.top}px`];\n        if (\"left\" in position) {\n            style.push(`left:${position.left}px`);\n        } else {\n            style.push(`right:${position.right}px`);\n        }\n        return style.join(\";\");\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\n\nexport class GanttRowProgressBar extends Component {\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                hoveredRowId: [String, { value: null }],\n            },\n        },\n        rowId: String,\n        progressBar: {\n            type: Object,\n            shape: {\n                max_value: Number,\n                max_value_formatted: String,\n                ratio: Number,\n                value_formatted: String,\n                warning: { type: String, optional: true },\n                \"*\": true,\n            },\n        },\n    };\n    static template = \"web_gantt.GanttRowProgressBar\";\n\n    get show() {\n        const { reactive, rowId } = this.props;\n        return reactive.hoveredRowId === rowId || isMobileOS() || hasTouch();\n    }\n\n    get status() {\n        const { ratio } = this.props.progressBar;\n        return ratio > 100 ? \"danger\" : ratio > 0 ? \"success\" : null;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nfunction _mockGetGanttData(params) {\n    const lazy = !params.limit && !params.offset && params.groupby.length === 1;\n    let { groups, length } = this._mockWebReadGroup({\n        ...params,\n        lazy,\n        fields: [\"__record_ids:array_agg(id)\"],\n    });\n    if (params.limit) {\n        // we don't care about pager feature in sample mode\n        // but we want to present something coherent\n        groups = groups.slice(0, params.limit);\n        length = groups.length;\n    }\n    groups.forEach((g) => (g.__record_ids = g.id)); // the sample server does not use the key __record_ids\n\n    const recordIds = [];\n    for (const group of groups) {\n        recordIds.push(...(group.__record_ids || []));\n    }\n\n    const { records } = this._mockWebSearchReadUnity({\n        model: params.model,\n        domain: [[\"id\", \"in\", recordIds]],\n        context: params.context,\n        specification: params.read_specification,\n    });\n\n    const unavailabilities = {};\n    for (const fieldName of params.unavailability_fields || []) {\n        unavailabilities[fieldName] = {};\n    }\n\n    const progress_bars = {};\n    for (const fieldName of params.progress_bar_fields || []) {\n        progress_bars[fieldName] = {};\n    }\n\n    return { groups, length, records, unavailabilities, progress_bars };\n}\n\nregistry.category(\"sample_server\").add(\"get_gantt_data\", _mockGetGanttData);\n", "import { registry } from \"@web/core/registry\";\nimport { scrollSymbol } from \"@web/search/action_hook\";\nimport { GanttArchParser } from \"./gantt_arch_parser\";\nimport { GanttController } from \"./gantt_controller\";\nimport { GanttModel } from \"./gantt_model\";\nimport { GanttRenderer } from \"./gantt_renderer\";\nimport { omit } from \"@web/core/utils/objects\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const ganttView = {\n    type: \"gantt\",\n    Controller: GanttController,\n    Renderer: GanttRenderer,\n    Model: GanttModel,\n    ArchParser: GanttArchParser,\n    searchMenuTypes: [\"filter\", \"groupBy\", \"favorite\"],\n    buttonTemplate: \"web_gantt.GanttView.Buttons\",\n\n    props: (genericProps, view, config) => {\n        const modelParams = {};\n        let scrollPosition;\n        if (genericProps.state) {\n            scrollPosition = genericProps.state[scrollSymbol];\n            modelParams.metaData = genericProps.state.metaData;\n            modelParams.displayParams = genericProps.state.displayParams;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n            const parser = new view.ArchParser();\n            const archInfo = parser.parse(arch);\n\n            let formViewId = archInfo.formViewId;\n            if (!formViewId) {\n                const formView = config.views.find((v) => v[1] === \"form\");\n                if (formView) {\n                    formViewId = formView[0];\n                }\n            }\n\n            modelParams.metaData = {\n                ...omit(archInfo, \"displayMode\"),\n                fields,\n                resModel,\n                formViewId,\n            };\n            modelParams.displayParams = {\n                displayMode: archInfo.displayMode,\n            };\n        }\n\n        return {\n            ...genericProps,\n            modelParams,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            scrollPosition,\n        };\n    },\n};\n\nviewRegistry.add(\"gantt\", ganttView);\n", "/* @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { INTERVALS, MODES, TIMELINES } from \"./cohort_model\";\n\nexport class CohortArchParser {\n    parse(arch, fields) {\n        const archInfo = {\n            fieldAttrs: {},\n            widgets: {},\n        };\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"cohort\": {\n                    if (node.hasAttribute(\"disable_linking\")) {\n                        archInfo.disableLinking = exprToBoolean(\n                            node.getAttribute(\"disable_linking\")\n                        );\n                    }\n                    const title = node.getAttribute(\"string\");\n                    if (title) {\n                        archInfo.title = title;\n                    }\n                    const dateStart = node.getAttribute(\"date_start\");\n                    if (dateStart) {\n                        archInfo.dateStart = dateStart;\n                        archInfo.dateStartString = fields[dateStart].string;\n                    } else {\n                        throw new Error(_t('Cohort view has not defined \"date_start\" attribute.'));\n                    }\n                    const dateStop = node.getAttribute(\"date_stop\");\n                    if (dateStop) {\n                        archInfo.dateStop = dateStop;\n                        archInfo.dateStopString = fields[dateStop].string;\n                    } else {\n                        throw new Error(_t('Cohort view has not defined \"date_stop\" attribute.'));\n                    }\n                    const mode = node.getAttribute(\"mode\") || \"retention\";\n                    if (mode && MODES.includes(mode)) {\n                        archInfo.mode = mode;\n                    } else {\n                        throw new Error(\n                            _t(\n                                \"The argument %(mode)s is not a valid mode. Here are the modes: %(modes)s\",\n                                { mode, modes: MODES }\n                            )\n                        );\n                    }\n                    const timeline = node.getAttribute(\"timeline\") || \"forward\";\n                    if (timeline && TIMELINES.includes(timeline)) {\n                        archInfo.timeline = timeline;\n                    } else {\n                        throw new Error(\n                            _t(\n                                \"The argument %(timeline)s is not a valid timeline. Here are the timelines: %(timelines)s\",\n                                { timeline, timelines: TIMELINES }\n                            )\n                        );\n                    }\n                    archInfo.measure = node.getAttribute(\"measure\") || \"__count\";\n                    const interval = node.getAttribute(\"interval\") || \"day\";\n                    if (interval && interval in INTERVALS) {\n                        archInfo.interval = interval;\n                    } else {\n                        throw new Error(\n                            _t(\n                                \"The argument %(interval)s is not a valid interval. Here are the intervals: %(intervals)s\",\n                                { interval, intervals: INTERVALS }\n                            )\n                        );\n                    }\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n\n                    archInfo.fieldAttrs[fieldName] = {};\n                    if (node.hasAttribute(\"string\")) {\n                        archInfo.fieldAttrs[fieldName].string = node.getAttribute(\"string\");\n                    }\n                    if (\n                        node.getAttribute(\"invisible\") === \"True\" ||\n                        node.getAttribute(\"invisible\") === \"1\"\n                    ) {\n                        archInfo.fieldAttrs[fieldName].isInvisible = true;\n                        break;\n                    }\n                    if (node.hasAttribute(\"widget\")) {\n                        archInfo.widgets[fieldName] = node.getAttribute(\"widget\");\n                    }\n                }\n            }\n        });\n        return archInfo;\n    }\n}\n", "/* @odoo-module */\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, toRaw, useRef } from \"@odoo/owl\";\n\nexport class CohortController extends Component {\n    static template = \"web_cohort.CohortView\";\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.model = useModelWithSampleData(this.props.Model, toRaw(this.props.modelParams));\n\n        useSetupAction({\n            rootRef: useRef(\"root\"),\n            getLocalState: () => {\n                return { metaData: this.model.metaData };\n            },\n            getContext: () => this.getContext(),\n        });\n    }\n\n    getContext() {\n        const { measure, interval } = this.model.metaData;\n        return { cohort_measure: measure, cohort_interval: interval };\n    }\n\n    /**\n     * @param {Object} row\n     */\n    onRowClicked(row) {\n        if (row.value === undefined || this.model.metaData.disableLinking) {\n            return;\n        }\n\n        const context = Object.assign({}, this.model.searchParams.context);\n        const domain = row.domain;\n        const views = {};\n        for (const [viewId, viewType] of this.env.config.views || []) {\n            views[viewType] = viewId;\n        }\n        function getView(viewType) {\n            return [context[`${viewType}_view_id`] || views[viewType] || false, viewType];\n        }\n        const actionViews = [getView(\"list\"), getView(\"form\")];\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: this.model.metaData.title,\n            res_model: this.model.metaData.resModel,\n            views: actionViews,\n            view_mode: \"list\",\n            target: \"current\",\n            context: context,\n            domain: domain,\n        });\n    }\n}\n", "/* @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const MODES = [\"retention\", \"churn\"];\nexport const TIMELINES = [\"forward\", \"backward\"];\nexport const INTERVALS = {\n    day: _t(\"Day\"),\n    week: _t(\"Week\"),\n    month: _t(\"Month\"),\n    year: _t(\"Year\"),\n};\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nexport class CohortModel extends Model {\n    /**\n     * @override\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _load = this._load.bind(this);\n        this._load = (...args) => {\n            return this.race.add(_load(...args));\n        };\n\n        this.metaData = params;\n        this.data = null;\n        this.searchParams = null;\n        this.intervals = INTERVALS;\n\n        const activeInterval = browser.localStorage.getItem(this.storageKey) || params.interval;\n        if (Object.keys(this.intervals).includes(activeInterval)) {\n            this.metaData.interval = activeInterval;\n        }\n    }\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    load(searchParams) {\n        const { comparison, context, domain } = searchParams;\n        this.searchParams = { context };\n        if (comparison) {\n            this.searchParams.domains = comparison.domains;\n        } else {\n            this.searchParams.domains = [{ arrayRepr: domain, description: null }];\n        }\n        const { cohort_interval, cohort_measure } = searchParams.context;\n        this.metaData.interval = cohort_interval || this.metaData.interval;\n\n        this.metaData.measure = processMeasure(cohort_measure) || this.metaData.measure;\n        this.metaData.measures = computeReportMeasures(\n            this.metaData.fields,\n            this.metaData.fieldAttrs,\n            [this.metaData.measure],\n            { sumAggregatorOnly: true }\n        );\n        return this._load(this.metaData);\n    }\n\n    get storageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.data.some((data) => data.rows.length > 0);\n    }\n\n    /**\n     * @param {Object} params\n     */\n    async updateMetaData(params) {\n        Object.assign(this.metaData, params);\n        browser.localStorage.setItem(this.storageKey, this.metaData.interval);\n        await this._load(this.metaData);\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     * @param {Object} metaData\n     */\n    async _load(metaData) {\n        this.data = await this.keepLast.add(this._fetchData(metaData));\n        for (const i in this.data) {\n            this.data[i].title = this.searchParams.domains[i].description;\n            this.data[i].rows.forEach((row) => {\n                row.columns = row.columns.filter((col) => col.percentage !== \"\");\n            });\n        }\n    }\n\n    /**\n     * @protected\n     * @param {Object} metaData\n     */\n    async _fetchData(metaData) {\n        return Promise.all(\n            this.searchParams.domains.map(({ arrayRepr: domain }) => {\n                return this.orm.call(metaData.resModel, \"get_cohort_data\", [], {\n                    date_start: metaData.dateStart,\n                    date_stop: metaData.dateStop,\n                    measure: metaData.measure,\n                    interval: metaData.interval,\n                    domain: domain,\n                    mode: metaData.mode,\n                    timeline: metaData.timeline,\n                    context: this.searchParams.context,\n                });\n            })\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatPercentage } from \"@web/views/fields/formatters\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { ViewScaleSelector } from \"@web/views/view_components/view_scale_selector\";\nimport { download } from \"@web/core/network/download\";\nimport { ReportViewMeasures } from \"@web/views/view_components/report_view_measures\";\n\nconst formatters = registry.category(\"formatters\");\n\nexport class CohortRenderer extends Component {\n    static components = { Dropdown, DropdownItem, ViewScaleSelector, ReportViewMeasures };\n    static template = \"web_cohort.CohortRenderer\";\n    static props = [\"class\", \"model\", \"onRowClicked\"];\n\n    setup() {\n        this.model = this.props.model;\n    }\n\n    range(n) {\n        return Array.from({ length: n }, (_, i) => i);\n    }\n\n    getFormattedValue(value) {\n        const fieldName = this.model.metaData.measure;\n        const field = this.model.metaData.measures[fieldName];\n        let formatType = this.model.metaData.widgets[fieldName];\n        if (!formatType) {\n            const fieldType = field.type;\n            formatType = [\"many2one\", \"reference\"].includes(fieldType) ? \"integer\" : fieldType;\n        }\n        const formatter = formatters.get(formatType);\n        return formatter(value, field);\n    }\n\n    formatPercentage(value) {\n        return formatPercentage(value, { digits: [false, 1] });\n    }\n\n    getCellTitle(period, measure, count) {\n        return _t(\"Period: %(period)s\\n%(measure)s: %(count)s\", { period, measure, count });\n    }\n\n    get scales() {\n        return Object.fromEntries(\n            Object.entries(this.model.intervals).map(([s, d]) => [s, { description: d }])\n        );\n    }\n\n    /**\n     * @param {String} scale\n     */\n    setScale(scale) {\n        this.model.updateMetaData({\n            interval: scale,\n        });\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {string} param0.measure\n     */\n    onMeasureSelected({ measure }) {\n        this.model.updateMetaData({ measure });\n    }\n\n    /**\n     * Export cohort data in Excel file\n     */\n    async downloadExcel() {\n        const {\n            title,\n            resModel,\n            interval,\n            measure,\n            measures,\n            dateStartString,\n            dateStopString,\n            timeline,\n        } = this.model.metaData;\n        const { domains } = this.model.searchParams;\n        const data = {\n            title: title,\n            model: resModel,\n            interval_string: this.model.intervals[interval].toString(), // intervals are lazy-translated\n            measure_string: measures[measure].string,\n            date_start_string: dateStartString,\n            date_stop_string: dateStopString,\n            timeline: timeline,\n            rangeDescription: domains[0].description,\n            report: this.model.data[0],\n            comparisonRangeDescription: domains[1] && domains[1].description,\n            comparisonReport: this.model.data[1],\n        };\n        this.env.services.ui.block();\n        try {\n            // FIXME: [SAD/JPP] some data seems to be missing from the export in master. (check the python)\n            await download({\n                url: \"/web/cohort/export\",\n                data: { data: new Blob([JSON.stringify(data)], { type: \"application/json\" }) },\n            });\n        } finally {\n            this.env.services.ui.unblock();\n        }\n    }\n}\n", "/* @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { CohortController } from \"./cohort_controller\";\nimport { CohortRenderer } from \"./cohort_renderer\";\nimport { CohortArchParser } from \"./cohort_arch_parser\";\nimport { CohortModel } from \"./cohort_model\";\n\nexport const cohortView = {\n    type: \"cohort\",\n    buttonTemplate: \"web_cohort.CohortView.Buttons\",\n    searchMenuTypes: [\"filter\", \"comparison\", \"favorite\"],\n    Model: CohortModel,\n    ArchParser: CohortArchParser,\n    Controller: CohortController,\n    Renderer: CohortRenderer,\n\n    props: (genericProps, view) => {\n        let modelParams;\n        if (genericProps.state) {\n            modelParams = genericProps.state.metaData;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n            const { ArchParser } = view;\n            const archInfo = new ArchParser().parse(arch, fields);\n            modelParams = {\n                dateStart: archInfo.dateStart,\n                dateStartString: archInfo.dateStartString,\n                dateStop: archInfo.dateStop,\n                dateStopString: archInfo.dateStopString,\n                fieldAttrs: archInfo.fieldAttrs,\n                fields: fields,\n                interval: archInfo.interval,\n                measure: archInfo.measure,\n                mode: archInfo.mode,\n                resModel: resModel,\n                timeline: archInfo.timeline,\n                title: archInfo.title,\n                disableLinking: Boolean(archInfo.disableLinking),\n                widgets: archInfo.widgets,\n            };\n        }\n\n        return {\n            ...genericProps,\n            modelParams,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"cohort\", cohortView);\n", "/** @odoo-module */\n\nimport { parseDate } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { SampleServer } from \"@web/model/sample_server\";\n\n/**\n * This function mocks calls to the 'get_cohort_data' method. It is\n * registered to the SampleServer's mockRegistry, so it is called with a\n * SampleServer instance as \"this\".\n * @private\n * @param {Object} params\n * @param {string} params.model\n * @param {Object} params.kwargs\n * @returns {Object}\n */\nfunction _mockGetCohortData(params) {\n    const { model, date_start, interval, measure, mode, timeline } = params;\n\n    const columns_avg = {};\n    const rows = [];\n    let initialChurnValue = 0;\n\n    const groups = this._mockReadGroup({\n        model,\n        fields: [date_start],\n        groupBy: [date_start + \":\" + interval],\n    });\n    const totalCount = groups.length;\n    let totalValue = 0;\n    for (const group of groups) {\n        const format = SampleServer.FORMATS[interval];\n        const displayFormat = SampleServer.DISPLAY_FORMATS[interval];\n        const date = parseDate(group[date_start + \":\" + interval], { format });\n        const now = luxon.DateTime.local();\n        let colStartDate = date;\n        if (timeline === \"backward\") {\n            colStartDate = colStartDate.plus({ [`${interval}s`]: -15 });\n        }\n\n        let value =\n            measure === \"__count\"\n                ? this._getRandomInt(SampleServer.MAX_INTEGER)\n                : this._generateFieldValue(model, measure);\n        value = value || 25;\n        totalValue += value;\n        let initialValue = value;\n        let max = value;\n\n        const columns = [];\n        for (let column = 0; column <= 15; column++) {\n            if (!columns_avg[column]) {\n                columns_avg[column] = { percentage: 0, count: 0 };\n            }\n            if (colStartDate.plus({ [`${interval}s`]: column }) > now) {\n                columns.push({ value: \"-\", churn_value: \"-\", percentage: \"\" });\n                continue;\n            }\n            let colValue = 0;\n            if (max > 0) {\n                colValue = Math.min(Math.round(Math.random() * max), max);\n                max -= colValue;\n            }\n            if (timeline === \"backward\" && column === 0) {\n                initialValue = Math.min(Math.round(Math.random() * value), value);\n                initialChurnValue = value - initialValue;\n            }\n            const previousValue = column === 0 ? initialValue : columns[column - 1].value;\n            const remainingValue = previousValue - colValue;\n            const previousChurnValue =\n                column === 0 ? initialChurnValue : columns[column - 1].churn_value;\n            const churn_value = colValue + previousChurnValue;\n            let percentage = value ? parseFloat(remainingValue / value) : 0;\n            if (mode === \"churn\") {\n                percentage = 1 - percentage;\n            }\n            percentage = Number((100 * percentage).toFixed(1));\n            columns_avg[column].percentage += percentage;\n            columns_avg[column].count += 1;\n            columns.push({\n                value: remainingValue,\n                churn_value,\n                percentage,\n                period: column, // used as a t-key but we don't care about value itself\n            });\n        }\n        const keepRow = columns.some((c) => c.percentage !== \"\");\n        if (keepRow) {\n            rows.push({ date: date.toFormat(displayFormat), value, columns });\n        }\n    }\n    const avg_value = totalCount ? totalValue / totalCount : 0;\n    const avg = { avg_value, columns_avg };\n    return { rows, avg };\n}\n\nregistry.category(\"sample_server\").add(\"get_cohort_data\", _mockGetCohortData);\n", "import { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\nimport { GraphController } from \"@web/views/graph/graph_controller\";\nimport { HrActionHelper } from \"@hr/views/hr_action_helper\";\n\nexport class HrGraphController extends GraphController {\n    static template = \"hr.GraphView\";\n    static components = { ...GraphController.components, HrActionHelper };\n}\nexport const HrGraphView = {\n    ...graphView,\n    Controller: HrGraphController,\n};\n\nregistry.category(\"views\").add(\"hr_graph_view\", HrGraphView);\n", "import { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\nimport { PivotController } from \"@web/views/pivot/pivot_controller\";\nimport { HrActionHelper } from \"@hr/views/hr_action_helper\";\n\nexport class HrPivotController extends PivotController {\n    static template = \"hr.PivotView\";\n    static components = { ...PivotController.components, HrActionHelper };\n}\nexport const HrPivotView = {\n    ...pivotView,\n    Controller: HrPivotController,\n};\n\nregistry.category(\"views\").add(\"hr_pivot_view\", HrPivotView);\n", "/** @odoo-module */\n\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { stringToOrderBy } from \"@web/search/utils/order_by\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions } from \"@web/views/utils\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nexport class HierarchyArchParser {\n    parse(xmlDoc, models, modelName) {\n        const archInfo = {\n            activeActions: getActiveActions(xmlDoc),\n            defaultOrder: stringToOrderBy(xmlDoc.getAttribute(\"default_order\") || null),\n            draggable: false,\n            icon: \"fa-share-alt fa-rotate-90 align-text-top\",\n            parentFieldName: \"parent_id\",\n            fieldNodes: {},\n            templateDocs: {},\n            xmlDoc,\n        };\n        const fieldNextIds = {};\n        const fields = models[modelName].fields;\n\n        visitXML(xmlDoc, (node) => {\n            if (node.hasAttribute(\"t-name\")) {\n                archInfo.templateDocs[node.getAttribute(\"t-name\")] = node;\n                return;\n            }\n            if (node.tagName === \"hierarchy\") {\n                if (node.hasAttribute(\"parent_field\")) {\n                    const parentFieldName = node.getAttribute(\"parent_field\");\n                    if (!(parentFieldName in fields)) {\n                        throw new Error(`The parent field set (${parentFieldName}) is not defined in the model (${modelName}).`);\n                    } else if (fields[parentFieldName].type !== \"many2one\") {\n                        throw new Error(`Invalid parent field, it should be a Many2One field.`);\n                    } else if (fields[parentFieldName].relation !== modelName) {\n                        throw new Error(`Invalid parent field, the co-model should be same model than the current one (expected: ${modelName}).`);\n                    }\n                    archInfo.parentFieldName = parentFieldName;\n                }\n                if (node.hasAttribute(\"child_field\")) {\n                    const childFieldName = node.getAttribute(\"child_field\");\n                    if (!(childFieldName in fields)) {\n                        throw new Error(`The child field set (${childFieldName}) is not defined in the model (${modelName}).`);\n                    } else if (fields[childFieldName].type !== \"one2many\") {\n                        throw new Error(`Invalid child field, it should be a One2Many field.`);\n                    } else if (fields[childFieldName].relation !== modelName) {\n                        throw new Error(`Invalid child field, the co-model should be same model than the current one (expected: ${modelName}).`);\n                    }\n                    archInfo.childFieldName = childFieldName;\n                }\n                if (node.hasAttribute(\"draggable\")) {\n                    archInfo.draggable = exprToBoolean(node.getAttribute(\"draggable\"));\n                }\n                if (node.hasAttribute(\"icon\")) {\n                    archInfo.icon = node.getAttribute(\"icon\");\n                }\n            } else if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"hierarchy\");\n                const name = fieldInfo.name;\n                if (!(name in fieldNextIds)) {\n                    fieldNextIds[name] = 0;\n                }\n                const fieldId = `${name}_${fieldNextIds[name]++}`;\n                archInfo.fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n            }\n        });\n\n        const cardDoc = archInfo.templateDocs[\"hierarchy-box\"];\n        if (!cardDoc) {\n            throw new Error(\"Missing 'hierarchy-box' template.\");\n        }\n\n        return archInfo;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { Field } from \"@web/views/fields/field\";\nimport { Record } from \"@web/model/record\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\n\nimport { HierarchyCompiler } from \"./hierarchy_compiler\";\nimport { getFormattedRecord } from \"@web/views/kanban/kanban_record\";\n\nexport class HierarchyCard extends Component {\n    static components = {\n        Record,\n        Field,\n        ViewButton,\n    };\n    static props = {\n        node: Object,\n        openRecord: Function,\n        archInfo: Object,\n        templates: Object,\n        classNames: { type: String, optional: true },\n    };\n    static defaultProps = {\n        classNames: \"\",\n    };\n    static template = \"web_hierarchy.HierarchyCard\";\n    static Compiler = HierarchyCompiler;\n\n    setup() {\n        const { templates } = this.props;\n        this.templates = useViewCompiler(this.constructor.Compiler, templates);\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n    }\n\n    get classNames() {\n        const classNames = [this.props.classNames];\n        if (this.props.node.nodes.length) {\n            classNames.push(\"o_hierarchy_node_unfolded\");\n        }\n        return classNames.join(\" \");\n    }\n\n    getRenderingContext(data) {\n        const record = getFormattedRecord(data.record);\n        return {\n            context: this.props.node.context,\n            JSON,\n            luxon,\n            record,\n            __comp__: Object.assign(Object.create(this), { this: this }),\n            __record__: data.record,\n        };\n    }\n\n    onGlobalClick(ev) {\n        if (ev.target.closest(\"button\")) {\n            return;\n        }\n        this.props.openRecord(this.props.node);\n    }\n\n    onClickArrowUp(ev) {\n        this.props.node.fetchParentNode();\n    }\n\n    onClickArrowDown(ev) {\n        if (this.props.node.nodes.length) {\n            this.props.node.collapseChildNodes();\n        } else {\n            this.props.node.showChildNodes();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { KanbanCompiler } from \"@web/views/kanban/kanban_compiler\";\n\nexport class HierarchyCompiler extends KanbanCompiler {\n    /**\n     * @override\n     * @param {Element} el\n     * @param {Object} params\n     * @returns {Element}\n     */\n    compileField(el, params) {\n        const fieldName = el.getAttribute(\"name\");\n        return super.compileField(el, {\n            ...(params || {}),\n            recordExpr: \"__record__\",\n            dataPointIdExpr: \"__comp__.props.node.id\",\n            formattedValueExpr: `record['${fieldName}'].value`,\n        });\n    }\n\n    compileButton(el, params) {\n        return super.compileButton(el, {\n            ...(params || {}),\n            recordExpr: \"__record__\",\n        });\n    }\n\n    /**\n     * Allow access to the record during compilation, to properly evaluate\n     * invisible on any hierarchy card nodes declared in the view.\n     *\n     * @override\n     */\n    compileNode(node, params = {}, evalInvisible = true) {\n        return super.compileNode(\n            node,\n            {\n                ...params,\n                recordExpr: \"__record__\",\n            },\n            evalInvisible\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { useModel } from \"@web/model/model\";\nimport { addFieldDependencies, extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { Layout } from \"@web/search/layout\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\n\nexport class HierarchyController extends Component {\n    static components = {\n        Layout,\n        CogMenu,\n        SearchBar,\n    };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        archInfo: Object,\n    };\n    static template = \"web_hierarchy.HierarchyView\";\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        const { parentFieldName, childFieldName } = this.props.archInfo;\n        const { activeFields, fields } = extractFieldsFromArchInfo(this.props.archInfo, this.props.fields);\n        addFieldDependencies(activeFields, fields, [{ name: parentFieldName }]);\n        this.model = useModel(this.props.Model, {\n            resModel: this.props.resModel,\n            activeFields,\n            defaultOrderBy: this.props.archInfo.defaultOrder,\n            fields,\n            parentFieldName,\n            childFieldName,\n        });\n        useBus(\n            this.model.bus,\n            \"update\",\n            () => {\n                this.render(true);\n            }\n        );\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: this.model.reload.bind(this.model),\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n    get displayNoContent() {\n        return this.model.resIds.length === 0;\n    }\n\n    async openRecord(node, mode) {\n        const activeIds = this.model.root.resIds;\n        this.props.selectRecord(node.resId, { activeIds, mode });\n    }\n\n    async beforeExecuteActionButton(clickParams) {}\n\n    async afterExecuteActionButton(clickParams) {}\n}\n", "/** @odoo-module */\n\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { Model } from \"@web/model/model\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\n\nlet nodeId = 0;\nlet forestId = 0;\nlet treeId = 0;\n\n/**\n * Get the id of the given many2one field value\n *\n * @param {false | [Number, string]} value many2one value\n * @returns {false | Number} id of the many2one\n */\nfunction getIdOfMany2oneField(value) {\n    return value && value[0];\n}\n\nexport class HierarchyNode {\n    /**\n     * Constructor of hierarchy node stored in hierarchy tree\n     *\n     * @param {HierarchyModel} model\n     * @param {Object} config\n     * @param {Object} data\n     * @param {HierarchyTree} tree\n     * @param {HierarchyNode} parentNode\n     * @param {Boolean} populateChildNodes\n     */\n    constructor(model, config, data, tree, parentNode = null, populateChildNodes = true) {\n        this.id = nodeId++;\n        this.data = data;\n        this.parentNode = parentNode;\n        this.tree = tree;\n        this.model = model;\n        this._config = config;\n        this.hidden = false;\n        tree.addNode(this);\n        if (populateChildNodes) {\n            this.populateChildNodes();\n        }\n    }\n\n    /**\n     * Get ancestor node\n     *\n     * @returns {HierarchyNode} ancestor node\n     */\n    get ancestorNode() {\n        return this.parentNode ? this.ancestorNode : this;\n    }\n\n    /**\n     * Is leaf?\n     *\n     * @returns {Boolean} False if the current node has node as child nodes, otherwise True.\n     */\n    get isLeaf() {\n        return !this.nodes.length;\n    }\n\n    /**\n     * Get forest of the current node\n     *\n     * @returns {HierarchyForest}\n     */\n    get forest() {\n        return this.tree.forest;\n    }\n\n    /**\n     * Get the resId of current node\n     *\n     * @returns {Number}\n     */\n    get resId() {\n        return this.data.id;\n    }\n\n    /**\n     * Get parent field name\n     *\n     * @returns {String}\n     */\n    get parentFieldName() {\n        return this.model.parentFieldName;\n    }\n\n    /**\n     * Get parent res id\n     *\n     * @returns {Number}\n     */\n    get parentResId() {\n        return this.parentNode?.resId || getIdOfMany2oneField(this.data[this.parentFieldName]);\n    }\n\n    /**\n     * Get child node res ids\n     *\n     * @returns {Number[]}\n     */\n    get childResIds() {\n        return this.nodes.length ? this.nodes.map((node) => node.resId) : this.data[this.childFieldName]?.map((d) => typeof d === \"number\" ? d : d.id) || [];\n    }\n\n    /**\n     * Get child field name\n     *\n     * @returns {String}\n     */\n    get childFieldName() {\n        return this.model.childFieldName || this.model.defaultChildFieldName;\n    }\n\n    /**\n     * Has child nodes?\n     *\n     * @returns {Boolean}\n     */\n    get hasChildren() {\n        return this.nodes.length > 0 || this.data[this.childFieldName]?.length > 0;\n    }\n\n    /**\n     * Can show parent node\n     *\n     * Knows if the parent node can be fetched and displayed inside the view\n     *\n     * @returns {Boolean} True if the current node has a parent node but it is not yet displayed and the data of the\n     *                    current node is not already displayed in another node.\n     */\n    get canShowParentNode() {\n        return Boolean(this.parentResId)\n            && !this.parentNode\n            && this.tree.forest.resIds.filter((resId) => resId === this.resId).length === 1;\n    }\n\n\n    /**\n     * Can show child nodes\n     *\n     * Knows if the child nodes can be fetched and displayed inside the view\n     *\n     * @returns {Boolean} True if the current node has child nodes but they are not yet displayed and the data of the\n     *                    current node is not already displayed in another node.\n     */\n    get canShowChildNodes() {\n        return this.hasChildren\n            && this.nodes.length === 0\n            && this.tree.forest.resIds.filter((resId) => resId === this.resId).length === 1;\n    }\n\n    get descendantNodes() {\n        const subNodes = [];\n        if (!this.isLeaf) {\n            subNodes.push(...this.nodes);\n            for (const node of this.nodes) {\n                if (node.descendantNodes.length) {\n                    subNodes.push(...node.descendantNodes);\n                }\n            }\n        }\n        return subNodes;\n    }\n\n    /**\n     * Get all descendants nodes parents. If the current node has descendants,\n     * it is also included in the result.\n     *\n     * @returns {Array} contains descendants parents in order of depth (closest\n     *          to root first).\n     */\n    get descendantsParentNodes() {\n        const descendantsParentNodes = [];\n        if (!this.isLeaf) {\n            descendantsParentNodes.push(this);\n            this.nodes.reduce((parents, node) => {\n                if (!node.isLeaf) {\n                    parents.push(...node.descendantsParentNodes);\n                }\n                return parents;\n            }, descendantsParentNodes);\n        }\n        return descendantsParentNodes;\n    }\n\n    /**\n     * Get all descendants nodes resIds\n     *\n     * @returns {Number[]}\n     */\n    get allSubsidiaryResIds() {\n        return this.descendantNodes.map((n) => n.resId);\n    }\n\n    /**\n     * Populate child nodes\n     *\n     * Uses to create child nodes of the current one according to its data.\n     */\n    populateChildNodes() {\n        this.nodes = [];\n        const children = this.data[this.childFieldName] || [];\n        if (\n            children.length\n            && children[0] instanceof Object\n            && this.tree.forest.resIds.filter((resId) => resId === this.resId).length === 1\n        ) {\n            this.createChildNodes(children);\n        }\n    }\n\n    /**\n     * create child nodes\n     *\n     * @param {Object[]} childNodesData data of child nodes to generate\n     */\n    createChildNodes(childNodesData) {\n        this.nodes = (childNodesData || this.data[this.childFieldName]).map(\n            (childData) =>\n                new HierarchyNode(\n                    this.model,\n                    this._config,\n                    childData,\n                    this.tree,\n                    this\n                )\n        );\n    }\n\n    removeParentNode() {\n        this.parentNode?.removeChildNode(this);\n        this.parentNode = null;\n        this.data[this.parentFieldName] = false;\n    }\n\n    /**\n     * Fetch parent node\n     */\n    async fetchParentNode() {\n        await this.model.fetchManager(this);\n    }\n\n    /**\n     * Fetch child nodes\n     */\n    async showChildNodes() {\n        await this.model.fetchSubordinates(this);\n    }\n\n    /**\n     * Collapse child nodes\n     *\n     * Removes the descendant nodes of the current one and stores\n     * the resIds of the child nodes in the data of the current one\n     * to know it has child nodes to be able to show them again\n     * when it is needed.\n     */\n    collapseChildNodes() {\n        const childrenData = [];\n        for (const childNode of this.nodes) {\n            childNode.data[this.childFieldName] = childNode.childResIds;\n            childrenData.push(childNode.data);\n        }\n        this.data[this.childFieldName] = childrenData;\n        this.removeChildNodes();\n        this.model.notify();\n    }\n\n    removeChildNode(node) {\n        node.removeChildNodes();\n        this.tree.removeNodes([node]);\n        this.nodes = this.nodes.filter((n) => n.id !== node.id);\n        this.data[this.childFieldName] = this.nodes.map((n) => n.data);\n    }\n\n    /**\n     * Remove descendant nodes of the current one\n     */\n    removeChildNodes() {\n        for (const childNode of this.nodes) {\n            if (!childNode.isLeaf) {\n                childNode.removeChildNodes();\n            }\n        }\n        this.tree.removeNodes(this.nodes);\n        this.nodes = [];\n    }\n\n    /**\n     * Set parent node to the current node\n     *\n     * @param {HierarchyNode} node parent node to set\n     */\n    setParentNode(node) {\n        this.parentNode = node;\n        node.addChildNode(this);\n        const tree = node.tree;\n        if (tree.root === this) {\n            tree.root = node;\n        } else if (this.tree.root === this) {\n            this.tree.removeRoot();\n            this.setTree(node.tree);\n        }\n    }\n\n    setTree(tree) {\n        this.tree = tree;\n        for (const childNode of this.nodes) {\n            childNode.setTree(tree);\n        }\n    }\n\n    /**\n     * Adds child node to the current node\n     *\n     * @param {HierarchyNode} node child node to add\n     */\n    addChildNode(node) {\n        this.nodes.push(node);\n        this.data[this.childFieldName].push(node.data);\n        this.tree.addNode(node);\n    }\n}\n\nexport class HierarchyTree {\n    /**\n     * Constructor\n     *\n     * @param {HierarchyModel} model\n     * @param {Object} config config of the model\n     * @param {Object} data root node data of the tree to create\n     * @param {HierarchyForest} forest hierarchy forest containing the tree to create\n     */\n    constructor(model, config, data, forest) {\n        this.id = treeId++;\n        this.nodePerNodeId = {};\n        this.forest = forest;\n        if (data) {\n            this.root = new HierarchyNode(model, config, data, this);\n            this.forest.nodePerNodeId = {\n                ...this.forest.nodePerNodeId,\n                ...this.nodePerNodeId,\n            };\n        }\n        this.model = model;\n        this._config = config;\n    }\n\n    /**\n     * Get node res ids inside the current tree\n     *\n     * @returns {Number}\n     */\n    get resIds() {\n        return Object.values(this.nodePerNodeId).map((node) => node.resId);\n    }\n\n    /**\n     * Add node inside the current tree\n     *\n     * @param {HierarchyNode} node node to add inside the current tree\n     */\n    addNode(node) {\n        this.nodePerNodeId[node.id] = node;\n        this.forest.addNode(node);\n    }\n\n    /**\n     * Remove nodes inside the current tree\n     *\n     * @param {HierarchyNode} nodes nodes to remove\n     */\n    removeNodes(nodes) {\n        const nodeIds = nodes.map((node) => node.id);\n        this.nodePerNodeId = Object.fromEntries(\n            Object.entries(this.nodePerNodeId)\n                .filter(\n                    ([nodeId,]) => !nodeIds.includes(Number(nodeId))\n                )\n            );\n        this.forest.removeNodes(nodes);\n    }\n\n    removeRoot() {\n        this.forest.removeTree(this);\n    }\n}\n\nexport class HierarchyForest {\n    /**\n     *\n     * @param {HierarchyModel} model\n     * @param {Object} config model config\n     * @param {Object[]} data list of tree root nodes data\n     */\n    constructor(model, config, data) {\n        this.id = forestId++;\n        this.nodePerNodeId = {};\n        this.trees = data.map((d) => new HierarchyTree(model, config, d, this));\n        this.model = model;\n        this._config = config;\n    }\n\n    /**\n     * Get node res ids containing inside the current forest\n     *\n     * @returns {Number}\n     */\n    get resIds() {\n        return Object.values(this.nodePerNodeId).map((node) => node.resId);\n    }\n\n    /**\n     * Get root node of all trees inside the current forest\n     *\n     * @returns {HierarchyNode[]} root nodes\n     */\n    get rootNodes() {\n        return this.trees.map((t) => t.root);\n    }\n\n    /**\n     * Add a node inside the current forest\n     *\n     * @param {HierarchyNode} node node to add inside the current forest\n     */\n    addNode(node) {\n        this.nodePerNodeId[node.id] = node;\n    }\n\n    /**\n     * Removes nodes inside the current forest\n     *\n     * @param {HierarchyNode} nodes nodes to remove inside the current forest\n     */\n    removeNodes(nodes) {\n        const nodeIds = nodes.map((node) => node.id);\n        this.nodePerNodeId = Object.fromEntries(\n            Object.entries(this.nodePerNodeId)\n                .filter(\n                    ([nodeId,]) => !nodeIds.includes(Number(nodeId))\n                )\n        );\n    }\n\n    addNewRootNode(node) {\n        const tree = new HierarchyTree(this.model, this._config, null, this);\n        tree.root = node;\n        node.tree = tree;\n        tree.addNode(node);\n        for (const subNode of node.descendantNodes) {\n            tree.addNode(subNode);\n        }\n        this.trees.push(tree);\n    }\n\n    removeTree(tree) {\n        this.nodePerNodeId = Object.fromEntries(\n            Object.entries(this.nodePerNodeId)\n                .filter(\n                    ([nodeId, ]) => !(nodeId in tree.nodePerNodeId)\n                )\n        );\n        this.trees = this.trees.filter((t) => t.id !== tree.id);\n    }\n}\n\nexport class HierarchyModel extends Model {\n    static services = [\"notification\"];\n\n    setup(params, { notification }) {\n        this.keepLast = new KeepLast();\n        this.mutex = new Mutex();\n        this.resModel = params.resModel;\n        this.fields = params.fields;\n        this.parentFieldName = params.parentFieldName;\n        this.childFieldName = params.childFieldName;\n        this.activeFields = params.activeFields;\n        this.defaultOrderBy = params.defaultOrderBy;\n        this.notification = notification;\n        this.config = {\n            domain: [],\n            isRoot: true,\n        };\n    }\n\n    /**\n     * Get parent field info\n     *\n     * @returns {Object} parent field info\n     */\n    get parentField() {\n        return this.fields[this.parentFieldName];\n    }\n\n    /**\n     * Get res ids of all nodes displayed in the view\n     *\n     * @returns {Number[]} resIds of all nodes displayed in the view\n     */\n    get resIds() {\n        return this.root?.resIds || [];\n    }\n\n    /**\n     * Get default child field name when no child field name is given to the view\n     *\n     * @returns {String} default child field name to use\n     */\n    get defaultChildFieldName() {\n        return \"__child_ids__\";\n    }\n\n    /**\n     * Get default domain to use, when no domain is given in the config\n     *\n     * @returns {import(\"@web/src/core/domain\").DomainListRepr} default domain\n     */\n    get defaultDomain() {\n        return [[this.parentFieldName, \"=\", false]];\n    }\n\n    /**\n     * Get the global domain of the view (which is the domain defined on the\n     * view without applying filters).\n     *\n     * @returns {import(\"@web/src/core/domain\").DomainListRepr} global domain\n     */\n    get globalDomain() {\n        if (!this.env.searchModel?.globalDomain.length) {\n            return [];\n        }\n        return new Domain(this.env.searchModel.globalDomain).toList(\n            this.env.searchModel.domainEvalContext\n        );\n    }\n\n    /**\n     * Get active fields name\n     *\n     * @returns {String[]} active fields name\n     */\n    get activeFieldNames() {\n        return Object.keys(this.activeFields);\n    }\n\n    /**\n     * Get fields to fetch\n     * @returns {String[]} fields to fetch\n     */\n    get fieldsToFetch() {\n        const fieldsToFetch = [\n            ...this.activeFieldNames,\n        ];\n        if (this.childFieldName) {\n            fieldsToFetch.push(this.childFieldName);\n        }\n        return fieldsToFetch;\n    }\n\n    get context() {\n        return {\n            bin_size: true,\n            ...(this.config.context || {}),\n        };\n    }\n\n    /**\n     * Load the config and data for hierarchy view\n     *\n     * @param {Object} params params to use to load data of hierarchy view\n     */\n    async load(params = {}) {\n        nodeId = forestId = treeId = 0;\n        const config = this._getNextConfig(this.config, params);\n        const data = await this.keepLast.add(this._loadData(config));\n        this.root = this._createRoot(config, data);\n        this.config = config;\n        this.notify();\n    }\n\n    /**\n     * Reload the current view with all currently loaded records\n     */\n    async reload() {\n        nodeId = forestId = treeId = 0;\n        const data = await this.keepLast.add(this._loadData(this.config, true));\n        this.root = this._createRoot(this.config, data);\n        this.notify({ scrollTarget: \"none\" });\n    }\n\n    /**\n     * @override\n     * Each notify should specify a scroll target (default is to scroll to the\n     * bottom).\n     */\n    notify(payload = { scrollTarget: \"bottom\" }) {\n        super.notify();\n        this.bus.trigger(\"hierarchyScrollTarget\", payload);\n    }\n\n    /**\n     * Fetch parent node of given node\n     * @param {HierarchyNode} node node to fetch its parent node\n     */\n    async fetchManager(node) {\n        if (this.root.trees.length > 1) { // reset the hierarchy\n            const treeExpanded = this._findTreeExpanded();\n            const resIdsToFetch = [node.parentResId, node.resId, ...node.allSubsidiaryResIds];\n            if (treeExpanded && treeExpanded.root.id !== node.id && treeExpanded.root.parentResId === node.parentResId) {\n                resIdsToFetch.push(...treeExpanded.root.allSubsidiaryResIds);\n            }\n            const config = {\n                ...this.config,\n                domain: [\"|\", [this.parentFieldName, \"=\", node.parentResId], [\"id\", \"in\", resIdsToFetch]],\n            }\n            const data = await this._loadData(config);\n            this.root = this._createRoot(config, data);\n            this.notify();\n            return;\n        }\n        const managerData = await this.keepLast.add(this._fetchManager(node));\n        if (managerData) {\n            const parentNode = new HierarchyNode(this, this.config, managerData, node.tree, null, false);\n            parentNode.createChildNodes();\n            node.setParentNode(parentNode);\n            this.notify();\n        }\n    }\n\n    /**\n     * Fetch child nodes of given node\n     *\n     * @param {HierarchyNode} node node to fetch its child nodes\n     */\n    async fetchSubordinates(node) {\n        const childFieldName = this.childFieldName || this.defaultChildFieldName;\n        const children = node.data[childFieldName];\n        if (children.length) {\n            const nodesToUpdate = [];\n            if (!(children[0] instanceof Object)) {\n                const allNodeResIds = this.root.resIds;\n                const existingChildResIds = children.filter((childResId) => allNodeResIds.includes(childResId))\n                if (existingChildResIds.length) { // special case with result found with the search view\n                    for (const tree of this.root.trees) {\n                        if (existingChildResIds.includes(tree.root.resId)) {\n                            nodesToUpdate.push(tree.root);\n                        }\n                    }\n                }\n                const data = await this.keepLast.add(this._fetchSubordinates(node, existingChildResIds));\n                if (data && data.length) {\n                    node.data[childFieldName] = data;\n                }\n            }\n            const nodeToCollapse = this._searchNodeToCollapse(node);\n            if (nodeToCollapse && !nodesToUpdate.includes(nodeToCollapse)) {\n                nodeToCollapse.collapseChildNodes();\n            }\n            node.populateChildNodes();\n            for (const n of nodesToUpdate) {\n                n.setParentNode(node);\n            }\n            this.notify();\n        }\n    }\n\n    /**\n     * Search node to collapse to be able to show the child nodes of node given in parameter\n     *\n     * @param {HierarchyNode} node node to show its child nodes.\n     * @returns {HierarchyNode | null} node found to collapse\n     */\n    _searchNodeToCollapse(node) {\n        const parentNode = node.parentNode;\n        let nodeToCollapse = null;\n        if (parentNode) {\n            nodeToCollapse = parentNode.nodes.find((n) => n.nodes.length);\n        } else {\n            const treeExpanded = this._findTreeExpanded();\n            if (treeExpanded) {\n                nodeToCollapse = treeExpanded.root;\n            }\n        }\n        return nodeToCollapse;\n    }\n\n    _findTreeExpanded() {\n        return this.root.trees.find((t) => t.root.nodes.length);\n    }\n\n    /**\n     * Get the next model config to use\n     *\n     * @param {Object} currentConfig current model config used\n     * @param {Object} params new params\n     * @returns {Object} new model config to use\n     */\n    _getNextConfig(currentConfig, params) {\n        const config = Object.assign({}, currentConfig);\n        config.context = \"context\" in params ? params.context : config.context;\n        if (\"domain\" in params) {\n            config.domain = params.domain;\n            if (this.isSearchDefaultOrEmpty() && config.context.hierarchy_res_id) {\n                config.domain = [[\"id\", \"=\", config.context.hierarchy_res_id]];\n                const globalDomain = this.globalDomain;\n                if (globalDomain.length) {\n                    config.domain = Domain.and([config.domain, globalDomain]);\n                }\n                // Just needed for the first load.\n                delete config.context.hierarchy_res_id;\n            }\n        }\n\n        // orderBy\n        config.orderBy = \"orderBy\" in params ? params.orderBy : config.orderBy;\n        // re-apply previous orderBy if not given (or no order)\n        if (!config.orderBy.length) {\n            config.orderBy = currentConfig.orderBy || [];\n        }\n        // apply default order if no order\n        if (this.defaultOrderBy && !config.orderBy.length) {\n            config.orderBy = this.defaultOrderBy;\n        }\n        return config;\n    }\n\n    /**\n     * Evaluate if the current search query is the default one.\n     *\n     * @returns {boolean}\n     */\n    isSearchDefaultOrEmpty() {\n        if (!this.env.searchModel) {\n            return true;\n        }\n        const isDisabledOptionalSearchMenuType = (type) => {\n            return (\n                [\"filter\", \"groupBy\", \"favorite\"].includes(type) &&\n                !this.env.searchModel.searchMenuTypes.has(type)\n            );\n        };\n        const activeSearchItems = this.env.searchModel.getSearchItems(\n            (item) => item.isActive && !isDisabledOptionalSearchMenuType(item.type)\n        );\n        if (!activeSearchItems.length) {\n            return true;\n        }\n        const defaultSearchItems = this.env.searchModel.getSearchItems(\n            (item) =>\n                item.isDefault &&\n                item.type !== \"favorite\" &&\n                !isDisabledOptionalSearchMenuType(item.type)\n        );\n        return JSON.stringify(defaultSearchItems) === JSON.stringify(activeSearchItems);\n    }\n\n    /**\n     * Load data for hierarchy view\n     *\n     * @param {Object} config model config\n     * @param {boolean} reload all currently loaded resIds instead of using\n     *        the config domain\n     * @returns {Object[]} main data for hierarchy view\n     */\n    async _loadData(config, reload = false) {\n        let onlyRoots = false;\n        let domain = config.domain;\n        const resIds = this.resIds;\n        if (reload && resIds.length > 0) {\n            domain = [[\"id\", \"in\", resIds]];\n        } else if (this.isSearchDefaultOrEmpty()) {\n            // If the current SearchModel query is the default one\n            // configured for the action or there is no search query, an\n            // additional constraint is added to only display \"root\"\n            // records (without a parent).\n            onlyRoots = true;\n            domain = !domain.length\n                ? this.defaultDomain\n                : Domain.and([this.defaultDomain, domain]).toList({});\n        }\n        const hierarchyRead = async () => {\n            return await this.orm.call(\n                this.resModel,\n                \"hierarchy_read\",\n                [\n                    domain,\n                    this.fieldsToFetch,\n                    this.parentFieldName,\n                    this.childFieldName,\n                    orderByToString(config.orderBy),\n                ],\n                { context: this.context }\n            );\n        };\n        let result = await hierarchyRead();\n        if (!result.length && onlyRoots) {\n            domain = config.domain;\n            result = await hierarchyRead();\n        }\n        return this._formatData(result);\n    }\n\n    _formatData(data) {\n        const dataStringified = JSON.stringify(data);\n        const recordsPerParentId = {};\n        const recordPerId = {};\n        for (const record of data) {\n            recordPerId[record.id] = record;\n            const parentId = getIdOfMany2oneField(record[this.parentFieldName]);\n            if (!(parentId.toString() in recordsPerParentId)) {\n                recordsPerParentId[parentId] = [];\n            }\n            recordsPerParentId[parentId].push(record);\n        }\n        const formattedData = [];\n        const recordIds = []; // to check if we have only one arborescence to display otherwise we display the data as the kanban view\n        for (const [parentId, records] of Object.entries(recordsPerParentId)) {\n            if (!parentId || !(parentId in recordPerId)) {\n                formattedData.push(...records);\n            } else {\n                const parentRecord = recordPerId[parentId];\n                if (recordIds.includes(parentRecord.id)) {\n                    return JSON.parse(dataStringified);\n                }\n                const ancestorId = getIdOfMany2oneField(parentRecord[this.parentFieldName]);\n                if (ancestorId in recordsPerParentId) {\n                    recordIds.push(...recordsPerParentId[ancestorId].map((r) => r.id));\n                }\n                parentRecord[this.childFieldName || this.defaultChildFieldName] = records;\n            }\n        }\n        if (!formattedData.length && data?.length) {\n            formattedData.push(recordPerId[Object.keys(recordsPerParentId)[0]]);\n        }\n        return formattedData;\n    }\n\n    /**\n     * Create forest\n     *\n     * @param {Object} config model config to use\n     * @param {Object[]} data root data\n     * @returns {HierarchyForest} forest hierarchy\n     */\n    _createRoot(config, data) {\n        return new HierarchyForest(this, config, data);\n    }\n\n    /**\n     * Fetch parent node and its children nodes data\n     *\n     * @param {HierarchyNode} node node to fetch its parent node\n     * @returns {Object} the parent node data with children data inside childFieldName\n     */\n    async _fetchManager(node, exclude_node=true) {\n        let domain = new Domain([\n            \"|\",\n                [\"id\", \"=\", node.parentResId],\n                [this.parentFieldName, \"=\", node.parentResId],\n        ]);\n        if (exclude_node) {\n            domain = Domain.and([\n                domain,\n                [[\"id\", \"!=\", node.resId]],\n            ])\n        }\n        const result = await this.orm.searchRead(\n            this.resModel,\n            domain.toList({}),\n            this.fieldsToFetch,\n            {\n                context: this.context,\n                order: orderByToString(this.config.orderBy),\n            },\n        );\n        let managerData = {};\n        const children = [];\n        for (const data of result) {\n            if (data.id === node.parentResId) {\n                managerData = data;\n            } else {\n                children.push(data);\n            }\n        }\n        if (!this.childFieldName) {\n            if (children.length) {\n                await this._fetchDescendants(children);\n            }\n        }\n        managerData[this.childFieldName || this.defaultChildFieldName] = children;\n        return managerData;\n    }\n\n    /**\n     * Fetch children nodes data for a given node\n     *\n     * @param {HierarchyNode} node node to fetch its children nodes\n     * @param {Array<number> | null} excludeResIds list of ids to exclude (because the nodes already exist)\n     * @returns {Object[]} list of child node data\n     */\n    async _fetchSubordinates(node, excludeResIds = null) {\n        let childrenResIds = node.data[this.childFieldName || this.defaultChildFieldName];\n        if (excludeResIds) {\n            childrenResIds = childrenResIds.filter((childResId) => !excludeResIds.includes(childResId));\n        }\n        const data = await this.orm.searchRead(\n            this.resModel,\n            [[\"id\", \"in\", childrenResIds]],\n            this.fieldsToFetch,\n            {\n                context: this.context,\n                order: orderByToString(this.config.orderBy),\n            },\n        )\n        if (!this.childFieldName) {\n            await this._fetchDescendants(data);\n        }\n        return data;\n    }\n\n    /**\n     * fetch descendants nodes resIds to know if the child nodes have descendants\n     *\n     * @param {Object[]} childrenData child nodes data to fetch its descendants\n     */\n    async _fetchDescendants(childrenData) {\n        const resIds = childrenData.map((d) => d.id);\n        if (resIds.length) {\n            const fetchChildren = await this.orm.readGroup(\n                this.resModel,\n                [[this.parentFieldName, \"in\", resIds]],\n                ['id:array_agg'],\n                [this.parentFieldName],\n                {\n                    context: this.context || {},\n                    orderby: orderByToString(this.config.orderBy),\n                },\n            );\n            const childIdsPerId = Object.fromEntries(\n                fetchChildren.map((r) => [r[this.parentFieldName][0], r.id])\n            );\n            for (const d of childrenData) {\n                if (d.id.toString() in childIdsPerId) {\n                    d[this.defaultChildFieldName] = childIdsPerId[d.id.toString()];\n                }\n            }\n        }\n    }\n\n    /**\n     * ORM call to update the parentId of a record during @see updateParentNode\n     * Can be overridden to not use \"write\".\n     *\n     * @param {HierarchyNode} node node related to the record which parentId\n     *        should be changed\n     * @param {Number} parentResId id of the new parent record\n     */\n    async updateParentId(node, parentResId = false) {\n        return this.orm.write(\n            this.resModel,\n            [node.resId],\n            { [this.parentFieldName]: parentResId },\n            { context: this.context }\n        );\n    }\n\n    /**\n     * @param {Number} nodeId of the node to update\n     * @param {Object} parentInfo\n     * @param {Number} [parentInfo.parentNodeId] nodeId of the parent\n     * @param {Number | false} [parentInfo.parentResId] resId of the parent\n     * @returns {Promise}\n     */\n    async updateParentNode(nodeId, { parentNodeId, parentResId }) {\n        const node = this.root.nodePerNodeId[nodeId];\n        const resId = node.resId;\n        // Validation.\n        if (!node) {\n            return;\n        }\n        const parentNode = parentNodeId ? this.root.nodePerNodeId[parentNodeId] : null;\n        parentResId = parentResId || parentNode?.resId || false;\n        const oldParentNode = node.parentNode;\n        if (\n            (parentNode && !this.validateUpdateParentNode(node, parentNode)) ||\n            parentNode?.resId === oldParentNode?.resId\n        ) {\n            return;\n        }\n        // Hide the node while waiting for the server response.\n        node.hidden = true;\n        this.notify({ scrollTarget: \"none\" });\n        // Update the parent server side.\n        await this.mutex.exec(async () => {\n            try {\n                await this.updateParentId(node, parentResId);\n            } catch (error) {\n                // Show the node again since the operation failed, don't update the view.\n                node.hidden = false;\n                this.notify({ scrollTarget: \"none\" });\n                throw error;\n            }\n        });\n        // Reload impacted records.\n        const domain = this.computeUpdateParentNodeDomain(node, parentResId, parentNode);\n        const data = await this.orm.searchRead(this.resModel, domain, this.fieldsToFetch, {\n            context: this.context,\n            order: orderByToString(this.config.orderBy),\n        });\n        const formattedData = this._formatData(data);\n        // Validate that data coming from the server is still compatible with the current\n        // configuration of the hierarchy.\n        for (const record of formattedData) {\n            if (getIdOfMany2oneField(record[this.parentFieldName]) !== parentResId) {\n                node.hidden = false;\n                this.notify({ scrollTarget: \"none\" });\n                this.notification.add(\n                    _t(\n                        `The parent of \"%s\" was successfully updated. Reloading records to account for other changes.`,\n                        node.data.display_name || node.data.name\n                    ),\n                    { type: \"success\" }\n                );\n                return this.reload();\n            }\n        }\n        // Handle the expanded tree.\n        let nodeToCollapse;\n        const treeExpanded = this._findTreeExpanded();\n        const expandedParentNodeIds =\n            treeExpanded?.root.descendantsParentNodes.map((node) => node.id) || [];\n        if (!node.isLeaf || !expandedParentNodeIds.includes(parentNode?.id)) {\n            // Handle cases where the expanded tree will be altered.\n            // If node is not a leaf, the new expanded tree will contain its descendants.\n            // If parentNode is not a parent in the current expanded tree, it will become one\n            // in the new expanded tree.\n            // Compute the depth of the parent of parentNode. That node is guaranteed to be a\n            // parent in the current expanded tree.\n            const depth = expandedParentNodeIds.findIndex(\n                (id) => id === parentNode?.parentNode?.id\n            );\n            if (depth === -1) {\n                // Drop as root or drop as the child of a root that is not part of the current\n                // expanded tree. The current expanded tree should be fully closed.\n                nodeToCollapse = treeExpanded?.root;\n            } else {\n                // Drop anywhere else (at a position that can be related to the expanded tree with\n                // the depth of the parent of parentNode). In that case the existing hierarchy is\n                // split at the depth of the parent, and will be completed by node's remaining\n                // expanded tree.\n                const nodeIdToCollapse = expandedParentNodeIds.at(depth + 1);\n                if (nodeIdToCollapse) {\n                    nodeToCollapse = treeExpanded?.nodePerNodeId[nodeIdToCollapse];\n                }\n            }\n        } else {\n            // Handle cases where node is a leaf dropped in the current expanded tree. In that case,\n            // the tree is kept open.\n            // Descendants of parentNode will always be reloaded to account for changes caused by\n            // the drop operation.\n            nodeToCollapse = parentNode;\n        }\n        // Update the view.\n        if (oldParentNode) {\n            oldParentNode.removeChildNode(node);\n        } else {\n            node.tree.removeNodes([node]);\n        }\n        nodeToCollapse?.collapseChildNodes();\n        if (!parentNode) {\n            // Drop as root, reset the hierarchy.\n            nodeId = forestId = treeId = 0;\n            this.root = this._createRoot(this.config, formattedData);\n        } else {\n            // Update parentNode data.\n            parentNode.data[this.childFieldName || this.defaultChildFieldName] = formattedData;\n            parentNode.populateChildNodes();\n        }\n        const newNodeId = Object.keys(this.root.nodePerNodeId).find((key) => {\n            return this.root.nodePerNodeId[key].resId === resId;\n        });\n        this.notify({ scrollTarget: newNodeId });\n    }\n\n    validateUpdateParentNode(node, parentNode) {\n        if (parentNode.resId === node.resId) {\n            this.notification.add(_t(\"The parent record cannot be the record dragged.\"), {\n                type: \"danger\",\n            });\n            return false;\n        } else if (node.allSubsidiaryResIds.includes(parentNode.resId)) {\n            this.notification.add(_t(\"Cannot change the parent because it will cause a cyclic.\"), {\n                type: \"danger\",\n            });\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Returns a domain to get a recordSet containing:\n     * - node.\n     * - all children under the new parent.\n     * - all descendants in the final expanded tree (after the operation), which\n     *   are at a depth impacted by the update @see updateParentNode (part\n     *   about the expanded tree).\n     *\n     * @param {HierarchyNode} node that is moving\n     * @param {Number | false} parentResId resId of the parent\n     * @param {HierarchyNode} [parentNode] which receives node as its child\n     *                        (undefined if node is dropped as a root).\n     * @returns {Array} domain\n     */\n    computeUpdateParentNodeDomain(node, parentResId, parentNode) {\n        const domainsOr = [[[\"id\", \"=\", node.resId]]];\n        // Include the new parent children (for ordering).\n        domainsOr.push([[this.parentFieldName, \"=\", parentResId]]);\n        if (!node.isLeaf) {\n            // Include node descendants (keep that part of the expanded tree).\n            const expandedTreeParentResIds = node.descendantsParentNodes.map((node) => node.resId);\n            domainsOr.push([[this.parentFieldName, \"in\", expandedTreeParentResIds]]);\n        } else if (!parentNode) {\n            // Keep the current expanded tree (if any) from its root if node is a leaf dropped as a\n            // root.\n            const expandedTreeParentResIds = node.tree.root.descendantsParentNodes.map(\n                (node) => node.resId\n            );\n            domainsOr.push([[this.parentFieldName, \"in\", expandedTreeParentResIds]]);\n        } else if (!parentNode.isLeaf) {\n            // Keep the current expanded tree (if any) from the target parent if node is a leaf.\n            const expandedTreeParentResIds = parentNode.descendantsParentNodes.map(\n                (node) => node.resId\n            );\n            domainsOr.push([[this.parentFieldName, \"in\", expandedTreeParentResIds]]);\n        }\n        let domain = Domain.or(domainsOr);\n        const globalDomain = this.globalDomain;\n        if (globalDomain.length) {\n            domain = Domain.and([domain, globalDomain]);\n        }\n        return domain.toList({});\n    }\n}\n", "/** @odoo-module */\n\nimport { onWillUnmount, reactive, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder\";\n\nconst hookParams = {\n    name: \"useHierarchyNodeDraggable\",\n    acceptedParams: {\n        rows: [String],\n    },\n    defaultParams: {\n        edgeScrolling: { speed: 20, threshold: 60 },\n        rows: null,\n    },\n    onComputeParams({ ctx, params }) {\n        // Row selector\n        ctx.rowSelector = params.rows || null;\n        if (ctx.rowSelector) {\n            ctx.fullSelector = `${ctx.rowSelector} ${ctx.fullSelector}`;\n        }\n    },\n    onDragStart(params) {\n        const { ctx, addListener, callHandler } = params;\n\n        const onElementPointerEnter = (ev) => {\n            const element = ev.currentTarget;\n            current.hierarchyElement = element;\n            callHandler(\"onElementEnter\", { element });\n        };\n\n        const onElementPointerLeave = (ev) => {\n            const element = ev.currentTarget;\n            current.hierarchyElement = null;\n            callHandler(\"onElementLeave\", { element });\n        };\n\n        const onRowPointerEnter = (ev) => {\n            const row = ev.currentTarget;\n            current.hierarchyRow = row;\n            callHandler(\"onRowEnter\", { row });\n        };\n\n        const onRowPointerLeave = (ev) => {\n            const row = ev.currentTarget;\n            current.hierarchyRow = null;\n            callHandler(\"onRowLeave\", { row });\n        };\n\n        const { ref, current, elementSelector, rowSelector } = ctx;\n\n        for (const rowEl of ref.el.querySelectorAll(rowSelector)) {\n            addListener(rowEl, \"pointerenter\", onRowPointerEnter);\n            addListener(rowEl, \"pointerleave\", onRowPointerLeave);\n        }\n\n        for (const siblingEl of ref.el.querySelectorAll(elementSelector)) {\n            if (siblingEl !== current.element) {\n                addListener(siblingEl, \"pointerenter\", onElementPointerEnter);\n                addListener(siblingEl, \"pointerleave\", onElementPointerLeave);\n            }\n        }\n\n        return pick(current, \"element\", \"row\");\n    },\n    onDragEnd({ ctx }) {\n        return pick(ctx.current, \"element\", \"row\", \"hierarchyRow\");\n    },\n    onDrop({ ctx }) {\n        const { current } = ctx;\n        const rowElement = current.hierarchyRow;\n        const element = current.hierarchyElement;\n        if ((rowElement && rowElement !== current.row) || element) {\n            return {\n                element: current.element,\n                row: current.row,\n                nextRow: rowElement && current.row !== rowElement ? rowElement : null,\n                newParentNode: element,\n            };\n        }\n    },\n    onWillStartDrag({ ctx }) {\n        const { current, rowSelector } = ctx;\n\n        if (rowSelector) {\n            current.row = current.element.closest(rowSelector);\n        }\n\n        return pick(current, \"element\", \"row\");\n    },\n};\n\nexport function useHierarchyNodeDraggable(params) {\n    const setupHooks = {\n        addListener: useExternalListener,\n        setup: useEffect,\n        teardown: onWillUnmount,\n        throttle: useThrottleForAnimation,\n        wrapState: reactive,\n    }\n    return makeDraggableHook({ ...hookParams, setupHooks })(params);\n}\n", "/** @odoo-module */\n\nimport { Component, useRef, onPatched } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\n\nimport { HierarchyCard } from \"./hierarchy_card\";\nimport { useHierarchyNodeDraggable } from \"./hierarchy_node_draggable\";\n\nexport class HierarchyRenderer extends Component {\n    static components = {\n        HierarchyCard,\n    };\n    static props = {\n        model: Object,\n        openRecord: Function,\n        archInfo: Object,\n        templates: Object,\n    };\n    static template = \"web_hierarchy.HierarchyRenderer\";\n\n    setup() {\n        this.rendererRef = useRef(\"renderer\");\n        this.notification = useService(\"notification\");\n        if (this.canDragAndDropRecord) {\n            useHierarchyNodeDraggable({\n                ref: this.rendererRef,\n                enable: this.draggable,\n                elements: \".o_hierarchy_node_container\",\n                handle: \".o_hierarchy_node\",\n                rows: \".o_hierarchy_row\",\n                ignore: \"button\",\n                onDragStart: ({ addClass, element }) => {\n                    addClass(element, \"o_hierarchy_dragged\");\n                    addClass(element.querySelector(\".o_hierarchy_node\"), \"shadow\");\n                },\n                onDragEnd: ({ removeClass, element, row, hierarchyRow }) => {\n                    removeClass(element, \"o_hierarchy_dragged\");\n                    if (row) {\n                        removeClass(row, \"o_hierarchy_hover\");\n                    }\n                    if (hierarchyRow) {\n                        removeClass(hierarchyRow, \"o_hierarchy_hover\");\n                    }\n                },\n                onDrop: (params) => {\n                    this.nodeDrop(params);\n                },\n                onElementEnter: ({ addClass, element }) => {\n                    addClass(element, \"o_hierarchy_hover\");\n                },\n                onElementLeave: ({ removeClass, element }) => {\n                    removeClass(element, \"o_hierarchy_hover\");\n                },\n                onRowEnter: ({ addClass, row }) => {\n                    addClass(row, \"o_hierarchy_hover\");\n                },\n                onRowLeave: ({ removeClass, row }) => {\n                    removeClass(row, \"o_hierarchy_hover\");\n                },\n            });\n        }\n        this.scrollTarget = \"none\";\n        useBus(this.props.model.bus, \"hierarchyScrollTarget\", (ev) => {\n            this.scrollTarget = ev.detail?.scrollTarget || \"none\";\n        });\n        onPatched(this.onPatched);\n    }\n\n    onPatched() {\n        if (this.scrollTarget === \"none\") {\n            return;\n        }\n        const row =\n            this.scrollTarget === \"bottom\"\n                ? this.rendererRef.el.querySelector(\":scope .o_hierarchy_row:last-child\")\n                : this.rendererRef.el\n                      .querySelector(\n                          `:scope .o_hierarchy_node[data-node-id=\"${this.scrollTarget}\"]`\n                      )\n                      ?.closest(\".o_hierarchy_row\");\n        this.scrollTarget = \"none\";\n        if (!row) {\n            return;\n        }\n        scrollTo(row, { behavior: \"smooth\" });\n    }\n\n    get canDragAndDropRecord() {\n        return this.draggable && !this.env.isSmall;\n    }\n\n    get draggable() {\n        return this.props.archInfo.draggable;\n    }\n\n    get rows() {\n        const rootNodes = this.props.model.root.rootNodes.filter((n) => !n.hidden);\n        const rows = [{ nodes: rootNodes }];\n        const processNode = (node) => {\n            if (!node.isLeaf) {\n                const subNodes = node.nodes.filter((n) => !n.hidden);\n                rows.push({ parentNode: node, nodes: subNodes });\n                for (const subNode of subNodes) {\n                    processNode(subNode);\n                }\n            }\n        };\n\n        for (const node of this.props.model.root.rootNodes) {\n            processNode(node);\n        }\n\n        return rows;\n    }\n\n    async nodeDrop({ element, row, nextRow, newParentNode }) {\n        let parentNodeId, parentResId;\n        if (newParentNode) {\n            parentNodeId = newParentNode.dataset.nodeId;\n        } else if (nextRow?.dataset.rowId !== row.dataset.rowId) {\n            parentNodeId = nextRow.dataset.parentNodeId;\n            if (!parentNodeId) {\n                const nodes = this.rows[nextRow.dataset.rowId].nodes || [];\n                if (nodes) {\n                    parentNodeId = nodes[0].parentNode?.id;\n                    if (!parentNodeId) {\n                        parentResId = nodes[0].parentResId;\n                        if (!nodes.every((node) => node.parentResId === parentResId)) {\n                            this.notification.add(\n                                _t(\"Impossible to update the parent node of the dragged node because no parent has been found.\"),\n                                {\n                                    type: \"danger\",\n                                }\n                            );\n                            return;\n                        }\n                    }\n                }\n            }\n        }\n        await this.props.model.updateParentNode(element.dataset.nodeId, { parentResId, parentNodeId });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { HierarchyArchParser } from \"./hierarchy_arch_parser\";\nimport { HierarchyController } from \"./hierarchy_controller\";\nimport { HierarchyModel } from \"./hierarchy_model\";\nimport { HierarchyRenderer } from \"./hierarchy_renderer\";\n\nexport const hierarchyView = {\n    type: \"hierarchy\",\n    ArchParser: HierarchyArchParser,\n    Controller: HierarchyController,\n    Model: HierarchyModel,\n    Renderer: HierarchyRenderer,\n    buttonTemplate: \"web_hierarchy.HierarchyButtons\",\n    searchMenuTypes: [\"filter\"],\n\n    props: (genericProps, view) => {\n        const { ArchParser, Model, Renderer, buttonTemplate: viewButtonTemplate } = view;\n        const { arch, relatedModels, resModel, buttonTemplate } = genericProps;\n        return {\n            ...genericProps,\n            archInfo: new ArchParser().parse(arch, relatedModels, resModel),\n            buttonTemplate: buttonTemplate || viewButtonTemplate,\n            Model,\n            Renderer,\n        };\n    }\n}\n\nregistry.category(\"views\").add(\"hierarchy\", hierarchyView);\n", "/** @odoo-module */\n\nimport { HierarchyCard } from \"@web_hierarchy/hierarchy_card\";\n\nexport class KnowledgeHierarchyCard extends HierarchyCard {\n    /**\n     * @override\n     * Add a context variable to be able to show/hide the section if a node\n     * is a root (in the hierarchy view).\n     */\n    getRenderingContext(data) {\n        const context = super.getRenderingContext(data);\n        return {\n            ...context,\n            isRoot: !this.props.node.parentNode,\n        };\n    }\n}\n", "/** @odoo-module */\n\nimport { HierarchyModel } from \"@web_hierarchy/hierarchy_model\";\n\nexport class KnowledgeHierarchyModel extends HierarchyModel {\n    /**\n     * @override\n     * Use the `move_to` method of the model instead of a simple `write` for\n     * some extra processing and validations.\n     */\n    async updateParentId(node, parentResId = false) {\n        return this.orm.call(\"knowledge.article\", \"move_to\", [node.resId], {\n            parent_id: parentResId,\n            category: parentResId ? false : node.data.category,\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport { HierarchyRenderer } from \"@web_hierarchy/hierarchy_renderer\";\nimport { KnowledgeHierarchyCard } from \"@knowledge/views/hierarchy/knowledge_hierarchy_card\";\n\nexport class KnowledgeHierarchyRenderer extends HierarchyRenderer {\n    static components = {\n        ...HierarchyRenderer.components,\n        HierarchyCard: KnowledgeHierarchyCard,\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { hierarchyView } from \"@web_hierarchy/hierarchy_view\";\nimport { KnowledgeHierarchyModel } from \"@knowledge/views/hierarchy/knowledge_hierarchy_model\";\nimport { KnowledgeHierarchyRenderer } from \"@knowledge/views/hierarchy/knowledge_hierarchy_renderer\";\n\nexport const KnowledgeHierarchyView = {\n    ...hierarchyView,\n    Model: KnowledgeHierarchyModel,\n    Renderer: KnowledgeHierarchyRenderer,\n    searchMenuTypes: [\"filter\", \"favorite\"],\n};\n\nregistry.category(\"views\").add(\"knowledge_hierarchy\", KnowledgeHierarchyView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { GraphModel } from \"@web/views/graph/graph_model\";\n\nexport class HelpdeskTicketGraphModel extends GraphModel {\n    /**\n     * @override\n     */\n    _getDefaultFilterLabel(field) {\n        if (field.fieldName === \"sla_deadline\") {\n            return _t(\"Deadline reached\");\n        }\n        if (field.fieldName == \"user_id\") {\n            return _t(\"Unassigned\");\n        }\n        return super._getDefaultFilterLabel(field);\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\nimport { HelpdeskTicketGraphModel } from \"./helpdesk_ticket_graph_model\";\n\nconst helpdeskTicketGraphView = {\n    ...graphView,\n    Model: HelpdeskTicketGraphModel,\n};\n\nregistry.category(\"views\").add(\"helpdesk_ticket_graph\", helpdeskTicketGraphView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { PivotModel } from \"@web/views/pivot/pivot_model\";\n\nexport class HelpdeskTicketPivotModel extends PivotModel {\n    /**\n     * @override\n     */\n    _getEmptyGroupLabel(fieldName) {\n        if (fieldName === \"sla_deadline\") {\n            return _t(\"Deadline reached\");\n        } else {\n            return super._getEmptyGroupLabel(fieldName);\n        }\n    }\n}\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\nimport { HelpdeskTicketPivotModel } from \"./helpdesk_ticket_pivot_model\";\n\nconst helpdeskTicketPivotView = {\n    ...pivotView,\n    Model: HelpdeskTicketPivotModel,\n};\n\nregistry.category(\"views\").add(\"helpdesk_ticket_pivot\", helpdeskTicketPivotView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { GraphRenderer } from \"@web/views/graph/graph_renderer\";\nimport { graphView } from \"@web/views/graph/graph_view\";\n\nexport class SkillsGraphRenderer extends GraphRenderer {\n    getScaleOptions() {\n        const scaleOptions = super.getScaleOptions();\n\n        if ('y' in scaleOptions) {\n            scaleOptions.y.suggestedMax = 100;\n        }\n\n        return scaleOptions;\n    }\n}\n\nexport const skillsGraphView = {\n    ...graphView,\n    Renderer: SkillsGraphRenderer,\n};\n\nregistry.category(\"views\").add(\"skills_graph\", skillsGraphView);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatFactor } from \"@web/views/fields/formatters\";\nimport { GridCell } from \"./grid_cell\";\n\nfunction formatter(value, options = {}) {\n    return formatFloatFactor(value, options);\n}\n\nexport class FloatFactorGridCell extends GridCell {\n    static props = {\n        ...GridCell.props,\n        factor: { type: Number, optional: true },\n    };\n\n    parse(value) {\n        const factorValue = value / this.factor;\n        return super.parse(factorValue.toString());\n    }\n\n    get factor() {\n        return this.props.factor || this.props.fieldInfo.options?.factor || 1;\n    }\n\n    get value() {\n        return super.value * this.factor;\n    }\n\n    get formattedValue() {\n        return formatter(this.value);\n    }\n}\n\nexport const floatFactorGridCell = {\n    component: FloatFactorGridCell,\n    formatter,\n};\n\nregistry.category(\"grid_components\").add(\"float_factor\", floatFactorGridCell);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { parseFloatTime } from \"@web/views/fields/parsers\";\nimport { formatFloatTime } from \"@web/views/fields/formatters\";\nimport { GridCell } from \"./grid_cell\";\n\nfunction formatter(value, options = {}) {\n    return formatFloatTime(value, { ...options, noLeadingZeroHour: true });\n}\n\nexport class FloatTimeGridCell extends GridCell {\n    get formattedValue() {\n        return formatter(this.value);\n    }\n\n    get inputMode() {\n        return \"text\";\n    }\n\n    parse(value) {\n        return parseFloatTime(value);\n    }\n}\n\nexport const floatTimeGridCell = {\n    component: FloatTimeGridCell,\n    formatter,\n};\n\nregistry.category(\"grid_components\").add(\"float_time\", floatTimeGridCell);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatFactor } from \"@web/views/fields/formatters\";\nimport { useGridCell, useMagnifierGlass } from \"@web_grid/hooks/grid_cell_hook\";\nimport { standardGridCellProps } from \"./grid_cell\";\n\nimport { Component, useRef, useState, useEffect } from \"@odoo/owl\";\n\nfunction formatter(value, options = {}) {\n    return formatFloatFactor(value, options);\n}\n\nexport class FloatToggleGridCell extends Component {\n    static props = {\n        ...standardGridCellProps,\n        factor: { type: Number, optional: true },\n    };\n    static template = \"web_grid.FloatToggleGridCell\";\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        this.buttonRef = useRef(\"toggleButton\");\n        this.magnifierGlassHook = useMagnifierGlass();\n        this.state = useState({\n            edit: this.props.editMode,\n            invalid: false,\n            cell: null,\n        });\n        useGridCell();\n\n        useEffect(\n            (buttonEl) => {\n                if (buttonEl) {\n                    buttonEl.focus();\n                }\n            },\n            () => [this.buttonRef.el]\n        );\n    }\n\n    get factor() {\n        return this.props.factor || this.props.fieldInfo.options?.factor || 1;\n    }\n\n    get range() {\n        return this.props.fieldInfo.options?.range || [0.0, 0.5, 1.0];\n    }\n\n    get value() {\n        return (this.state.cell.value || 0) * this.factor;\n    }\n\n    get formattedValue() {\n        return formatter(this.state.cell.value || 0, {\n            digits: this.props.fieldInfo.attrs?.digits || 2,\n            factor: this.factor,\n        });\n    }\n\n    isEditable(props = this.props) {\n        return (\n            !props.readonly && this.state.cell?.readonly === false && !this.state.cell.row.isSection\n        );\n    }\n\n    onChange() {\n        let currentIndex = this.range.indexOf(this.value);\n        currentIndex++;\n        if (currentIndex > this.range.length - 1) {\n            currentIndex = 0;\n        }\n        this.update(this.range[currentIndex] / this.factor);\n    }\n\n    update(value) {\n        this.state.cell.update(value);\n    }\n\n    onCellClick(ev) {\n        if (this.isEditable() && !this.state.edit && !ev.target.closest(\".o_grid_search_btn\")) {\n            this.onChange();\n            this.props.onEdit(true);\n        }\n    }\n\n    onKeyDown(ev) {\n        this.props.onKeyDown(ev, this.state.cell);\n    }\n}\n\nexport const floatToggleGridCell = {\n    component: FloatToggleGridCell,\n    formatter,\n};\n\nregistry.category(\"grid_components\").add(\"float_toggle\", floatToggleGridCell);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nimport { useNumpadDecimal } from \"@web/views/fields/numpad_decimal_hook\";\nimport { formatInteger } from \"@web/views/fields/formatters\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { parseInteger, parseFloat } from \"@web/views/fields/parsers\";\nimport { useInputHook } from \"@web_grid/hooks/input_hook\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useGridCell, useMagnifierGlass } from \"@web_grid/hooks/grid_cell_hook\";\n\nexport const standardGridCellProps = {\n    name: String,\n    classNames: String,\n    fieldInfo: Object,\n    readonly: { type: Boolean, optional: true },\n    editMode: { type: Boolean, optional: true },\n    reactive: {\n        type: Object,\n        shape: {\n            cell: [HTMLElement, { value: null }],\n        },\n    },\n    openRecords: Function,\n    onEdit: Function,\n    getCell: Function,\n    onKeyDown: { type: Function, optional: true },\n};\n\nexport class GridCell extends Component {\n    static template = \"web_grid.Cell\";\n    static props = standardGridCellProps;\n    static defaultProps = {\n        readonly: true,\n        editMode: false,\n    };\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        this.state = useState({\n            edit: this.props.editMode,\n            invalid: false,\n            cell: null,\n        });\n        this.discardChanges = false;\n        this.magnifierGlassHook = useMagnifierGlass();\n        this.inputRef = useInputHook({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: this.parse.bind(this),\n            notifyChange: this.onChange.bind(this),\n            commitChanges: this.saveEdition.bind(this),\n            onKeyDown: (ev) => this.props.onKeyDown(ev, this.state.cell),\n            discard: this.discard.bind(this),\n            setInvalid: () => {\n                this.state.invalid = true;\n            },\n            setDirty: () => {\n                this.state.invalid = false;\n            },\n            isInvalid: () => this.state.invalid,\n        });\n        useNumpadDecimal();\n\n        useGridCell();\n        useEffect(\n            (edit, inputEl, cellEl) => {\n                if (inputEl) {\n                    inputEl.value = this.formattedValue;\n                }\n                if (edit && inputEl) {\n                    inputEl.focus();\n                    if (inputEl.type === \"text\") {\n                        if (inputEl.selectionStart === null) {\n                            return;\n                        }\n                        if (inputEl.selectionStart === inputEl.selectionEnd) {\n                            inputEl.selectionStart = 0;\n                            inputEl.selectionEnd = inputEl.value.length;\n                        }\n                    }\n                }\n                this.discardChanges = false;\n            },\n            () => [this.state.edit, this.inputRef.el, this.props.reactive.cell]\n        );\n    }\n\n    get value() {\n        return this.state.cell?.value || 0;\n    }\n\n    get section() {\n        return this.row.getSection();\n    }\n\n    get row() {\n        return this.state.cell?.row;\n    }\n\n    get formattedValue() {\n        const { type, digits } = this.props.fieldInfo;\n        if (type === \"integer\") {\n            return formatInteger(this.value);\n        }\n        return formatFloat(this.value, { digits: digits || 2 });\n    }\n\n    get inputMode() {\n        return \"numeric\";\n    }\n\n    isEditable(props = this.props) {\n        return (\n            !props.readonly && this.state.cell?.readonly === false && !this.state.cell.row.isSection\n        );\n    }\n\n    parse(value) {\n        if (this.props.fieldInfo.type === \"integer\") {\n            return parseInteger(value);\n        }\n        return parseFloat(value);\n    }\n\n    onChange(value) {\n        if (!this.discardChanges) {\n            this.update(value);\n        }\n    }\n\n    update(value) {\n        this.state.cell.update(value);\n    }\n\n    saveEdition(value) {\n        const changesCommitted = (value || false) !== (this.state.cell.value || false);\n        if ((value || false) !== (this.state.cell?.value || false)) {\n            this.update(value);\n        }\n        this.props.onEdit(false);\n        return changesCommitted;\n    }\n\n    discard() {\n        this.discardChanges = true;\n        this.props.onEdit(false);\n    }\n\n    onCellClick(ev) {\n        if (this.isEditable() && !this.state.edit) {\n            this.discardChanges = false;\n            this.props.onEdit(true);\n        }\n    }\n}\n\nexport const integerGridCell = {\n    component: GridCell,\n    formatter: formatInteger,\n};\n\nregistry.category(\"grid_components\").add(\"integer\", integerGridCell);\n\nexport const floatGridCell = {\n    component: GridCell,\n    formatter: formatFloat,\n};\n\nregistry.category(\"grid_components\").add(\"float\", floatGridCell);\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nimport { GridCell } from \"../grid_cell\";\nimport { GridRow } from \"../grid_row/grid_row\";\n\nconst gridComponentRegistry = registry.category(\"grid_components\");\n\nexport class GridComponent extends Component {\n    static props = [\"name\", \"type\", \"isMeasure?\", \"component?\", \"*\"];\n    static template = \"web_grid.GridComponent\"\n\n    get gridComponent() {\n        if (this.props.component) {\n            return this.props.component;\n        }\n        if (gridComponentRegistry.contains(this.props.type)) {\n            return gridComponentRegistry.get(this.props.type).component;\n        }\n        if (this.props.isMeasure) {\n            console.warn(`Missing widget: ${this.props.type} for grid component`);\n            return GridCell;\n        }\n        return GridRow;\n    }\n\n    get gridComponentProps() {\n        const gridComponentProps = Object.fromEntries(\n            Object.entries(this.props).filter(\n                ([key,]) => key in this.gridComponent.props\n            )\n        );\n        gridComponentProps.classNames = `o_grid_component o_grid_component_${this.props.type} ${gridComponentProps.classNames || \"\"}`;\n        return gridComponentProps;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class GridRow extends Component {\n    static template = \"web_grid.GridRow\";\n    static props = {\n        name: String,\n        model: Object,\n        row: Object,\n        classNames: { type: String, optional: true },\n        context: { type: Object, optional: true },\n        style: { type: String, optional: true },\n        value: { optional: true },\n    };\n    static defaultProps = {\n        classNames: \"\",\n        context: {},\n        style: \"\",\n    };\n\n    get value() {\n        let value = 'value' in this.props ? this.props.value : this.props.row.initialRecordValues[this.props.name];\n        const fieldInfo = this.props.model.fieldsInfo[this.props.name];\n        if (fieldInfo.type === \"selection\") {\n            value = fieldInfo.selection.find(([key,]) => key === value)?.[1];\n        }\n        return value;\n    }\n}\n\nexport const gridRow = {\n    component: GridRow,\n};\n\nregistry\n    .category(\"grid_components\")\n    .add(\"selection\", gridRow)\n    .add(\"char\", gridRow);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { GridRow, gridRow } from \"../grid_row/grid_row\";\n\nexport class Many2OneGridRow extends GridRow {\n    static template = \"web_grid.Many2OneGridRow\";\n    static props = {\n        ...GridRow.props,\n        relation: { type: String, optional: true },\n        canOpen: { type: Boolean, optional: true },\n    }\n    static defaultProps = {\n        ...GridRow.defaultProps,\n        canOpen: true,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n    }\n\n    get relation() {\n        return this.props.relation || this.props.model.fieldsInfo[this.props.name].relation;\n    }\n\n    get urlRelation() {\n        if (!this.relation.includes(\".\")) {\n            return \"m-\" + this.relation;\n        }\n        return this.relation;\n    }\n\n    get displayName() {\n        return this.value && this.value[1].split(\"\\n\", 1)[0];\n    }\n\n    get extraLines() {\n        return this.value\n            ? this.value[1]\n                  .split(\"\\n\")\n                  .map((line) => line.trim())\n                  .slice(1)\n            : [];\n    }\n\n    get resId() {\n        return this.value && this.value[0];\n    }\n\n    async openAction() {\n        const action = await this.orm.call(this.relation, \"get_formview_action\", [[this.resId]], {\n            context: this.props.context,\n        });\n        await this.actionService.doAction(action);\n    }\n\n    onClick(ev) {\n        if (this.props.canOpen) {\n            ev.stopPropagation();\n            this.openAction();\n        }\n    }\n}\n\nexport const many2OneGridRow = {\n    ...gridRow,\n    component: Many2OneGridRow,\n};\n\nregistry.category(\"grid_components\").add(\"many2one\", many2OneGridRow);\n", "/** @odoo-module */\n\nimport { useComponent, useEffect } from \"@odoo/owl\";\n\nexport function useMagnifierGlass() {\n    const component = useComponent();\n    return {\n        onMagnifierGlassClick() {\n            const { context, domain, title } = component.state.cell;\n            component.props.openRecords(title, domain.toList(), context);\n        },\n    };\n}\n\nexport function useGridCell() {\n    const component = useComponent();\n    useEffect(\n        /** @param {HTMLElement | null} cellEl */\n        (cellEl) => {\n            if (!cellEl) {\n                component.state.cell = null;\n                return;\n            }\n            component.state.cell = component.props.getCell(\n                cellEl.dataset.row,\n                cellEl.dataset.column\n            );\n            Object.assign(component.rootRef.el.style, {\n                \"grid-row\": cellEl.style[\"grid-row\"],\n                \"grid-column\": cellEl.style[\"grid-column\"],\n                \"z-index\": 1,\n            });\n            component.rootRef.el.dataset.gridRow = cellEl.dataset.gridRow;\n            component.rootRef.el.dataset.gridColumn = cellEl.dataset.gridColumn;\n            cellEl.querySelector(\".o_grid_cell_readonly\").classList.add(\"d-none\");\n            component.rootRef.el.classList.toggle(\n                \"o_field_cursor_disabled\",\n                !component.state.cell.row.isSection && !component.isEditable()\n            );\n            component.rootRef.el.classList.toggle(\"fw-bold\", Boolean(component.state.cell.row.isSection));\n        },\n        () => [component.props.reactive.cell]\n    );\n}\n", "/** @odoo-module */\n\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport function useInputHook(params) {\n    const inputRef = params.ref || useRef(params.refName || \"input\");\n\n    /*\n     * A field is dirty if it is no longer sync with the model\n     * More specifically, a field is no longer dirty after it has *tried* to update the value in the model.\n     * An invalid value will therefore not be dirty even if the model will not actually store the invalid value.\n     */\n    let isDirty = false;\n\n    /**\n     * The last value that has been committed to the model.\n     * Not changed in case of invalid field value.\n     */\n    let lastSetValue = null;\n\n    /**\n     * When a user types, we need to set the field as dirty.\n     */\n    function onInput(ev) {\n        isDirty = ev.target.value !== lastSetValue;\n        if (params.setDirty) {\n            params.setDirty(isDirty);\n        }\n    }\n\n    /**\n     * On blur, we consider the field no longer dirty, even if it were to be invalid.\n     * However, if the field is invalid, the new value will not be committed to the model.\n     */\n    function onChange(ev) {\n        if (isDirty) {\n            isDirty = false;\n            let isInvalid = false;\n            let val = ev.target.value;\n            if (params.parse) {\n                try {\n                    val = params.parse(val);\n                } catch {\n                    if (params.setInvalid) {\n                        params.setInvalid();\n                    }\n                    isInvalid = true;\n                }\n            }\n\n            if (!isInvalid) {\n                params.notifyChange(val);\n                lastSetValue = ev.target.value;\n            }\n\n            if (params.setDirty) {\n                params.setDirty(isDirty);\n            }\n        }\n    }\n    function onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (params.discard && hotkey === \"escape\") {\n            params.discard();\n        } else if (params.commitChanges && [\"enter\", \"tab\", \"shift+tab\"].includes(hotkey)) {\n            commitChanges();\n        }\n        if (params.onKeyDown) {\n            params.onKeyDown(ev);\n        }\n    }\n\n    useEffect(\n        (inputEl) => {\n            if (inputEl) {\n                inputEl.addEventListener(\"input\", onInput);\n                inputEl.addEventListener(\"change\", onChange);\n                inputEl.addEventListener(\"keydown\", onKeydown);\n                return () => {\n                    inputEl.removeEventListener(\"input\", onInput);\n                    inputEl.removeEventListener(\"change\", onChange);\n                    inputEl.removeEventListener(\"keydown\", onKeydown);\n                };\n            }\n        },\n        () => [inputRef.el]\n    );\n\n    /**\n     * Sometimes, a patch can happen with possible a new value for the field\n     * If the user was typing a new value (isDirty) or the field is still invalid,\n     * we need to do nothing.\n     * If it is not such a case, we update the field with the new value.\n     */\n    useEffect(() => {\n        const isInvalid = params.isInvalid ? params.isInvalid() : false;\n        if (inputRef.el && !isDirty && !isInvalid) {\n            inputRef.el.value = params.getValue();\n            lastSetValue = inputRef.el.value;\n        }\n    });\n\n    function isUrgentSaved(urgent) {\n        if (params.isUrgentSaved) {\n            return params.isUrgentSaved(urgent);\n        }\n        return urgent;\n    }\n\n    /**\n     * Roughly the same as onChange, but called at more specific / critical times. (See bus events)\n     */\n    async function commitChanges(urgent) {\n        if (!inputRef.el) {\n            return;\n        }\n\n        isDirty = inputRef.el.value !== lastSetValue;\n        if (isDirty || isUrgentSaved(urgent)) {\n            let isInvalid = false;\n            isDirty = false;\n            let val = inputRef.el.value;\n            if (params.parse) {\n                try {\n                    val = params.parse(val);\n                } catch {\n                    isInvalid = true;\n                    if (urgent) {\n                        return;\n                    } else {\n                        params.setInvalid();\n                    }\n                }\n            }\n\n            if (isInvalid) {\n                return;\n            }\n\n            const result = params.commitChanges(val); // means change has been committed\n            if (result) {\n                lastSetValue = inputRef.el.value;\n                if (params.setDirty) {\n                    params.setDirty(isDirty);\n                }\n            }\n        }\n    }\n\n    return inputRef;\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { getActiveActions, processButton } from \"@web/views/utils\";\n\nexport class GridArchParser {\n    parse(xmlDoc, models, modelName) {\n        const archInfo = {\n            activeActions: getActiveActions(xmlDoc),\n            hideLineTotal: false,\n            hideColumnTotal: false,\n            hasBarChartTotal: false,\n            createInline: false,\n            displayEmpty: false,\n            buttons: [],\n            activeRangeName: \"\",\n            ranges: {},\n            sectionField: null,\n            rowFields: [],\n            columnFieldName: \"\",\n            measureField: {\n                name: \"__count\",\n                aggregator: \"sum\",\n                readonly: true,\n                string: _t(\"Count\"),\n            },\n            readonlyField: null,\n            widgetPerFieldName: {},\n            editable: false,\n            formViewId: false,\n        };\n        let buttonId = 0;\n\n        visitXML(xmlDoc, (node) => {\n            if (node.tagName === \"grid\") {\n                if (node.hasAttribute(\"hide_line_total\")) {\n                    archInfo.hideLineTotal = exprToBoolean(node.getAttribute(\"hide_line_total\"));\n                }\n                if (node.hasAttribute(\"hide_column_total\")) {\n                    archInfo.hideColumnTotal = exprToBoolean(\n                        node.getAttribute(\"hide_column_total\")\n                    );\n                }\n                if (node.hasAttribute(\"barchart_total\")) {\n                    archInfo.hasBarChartTotal = exprToBoolean(\n                        node.getAttribute(\"barchart_total\")\n                    );\n                }\n                if (node.hasAttribute(\"create_inline\")) {\n                    archInfo.createInline = exprToBoolean(node.getAttribute(\"create_inline\"));\n                }\n                if (node.hasAttribute(\"display_empty\")) {\n                    archInfo.displayEmpty = exprToBoolean(node.getAttribute(\"display_empty\"));\n                }\n                if (node.hasAttribute(\"action\") && node.hasAttribute(\"type\")) {\n                    archInfo.openAction = {\n                        name: node.getAttribute(\"action\"),\n                        type: node.getAttribute(\"type\"),\n                    };\n                }\n                if (node.hasAttribute(\"editable\")) {\n                    archInfo.editable = exprToBoolean(node.getAttribute(\"editable\"));\n                }\n                if (node.hasAttribute(\"form_view_id\")) {\n                    archInfo.formViewId = parseInt(node.getAttribute(\"form_view_id\"), 10);\n                }\n            } else if (node.tagName === \"field\") {\n                const fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n                const fieldInfo = models[modelName].fields[fieldName];\n                const type = node.getAttribute(\"type\") || \"row\";\n                const string = node.getAttribute(\"string\") || fieldInfo.string;\n                let invisible = node.getAttribute(\"invisible\") || 'False';\n                switch (type) {\n                    case \"row\":\n                        if (node.hasAttribute(\"widget\")) {\n                            archInfo.widgetPerFieldName[fieldName] = node.getAttribute(\"widget\");\n                        }\n                        if (\n                            node.hasAttribute(\"section\") &&\n                            exprToBoolean(node.getAttribute(\"section\")) &&\n                            !archInfo.sectionField\n                        ) {\n                            archInfo.sectionField = {\n                                name: fieldName,\n                                invisible,\n                            };\n                        } else {\n                            archInfo.rowFields.push({\n                                name: fieldName,\n                                invisible,\n                            });\n                        }\n                        break;\n                    case \"col\":\n                        archInfo.columnFieldName = fieldName;\n                        const { ranges, activeRangeName } = this._extractRanges(node);\n                        archInfo.ranges = ranges;\n                        archInfo.activeRangeName = activeRangeName;\n                        break;\n                    case \"measure\":\n                        if (node.hasAttribute(\"widget\")) {\n                            archInfo.widgetPerFieldName[fieldName] = node.getAttribute(\"widget\");\n                        }\n                        archInfo.measureField = {\n                            name: fieldName,\n                            aggregator: node.getAttribute(\"operator\") || fieldInfo.aggregator,\n                            string,\n                            readonly: exprToBoolean(node.getAttribute(\"readonly\")) || fieldInfo.readonly,\n                        };\n                        break;\n                    case \"readonly\":\n                        let groupOperator = fieldInfo.aggregator;\n                        if (node.hasAttribute(\"operator\")) {\n                            groupOperator = node.getAttribute(\"operator\");\n                        }\n                        archInfo.readonlyField = {\n                            name: fieldName,\n                            aggregator: groupOperator,\n                            string,\n                        };\n                        break;\n                }\n            } else if (node.tagName === \"button\") {\n                archInfo.buttons.push({\n                    ...processButton(node),\n                    type: \"button\",\n                    id: buttonId++,\n                });\n            }\n        });\n        archInfo.editable =\n            archInfo.editable &&\n            archInfo.measureField &&\n            !archInfo.measureField.readonly &&\n            archInfo.measureField.aggregator === \"sum\";\n        return archInfo;\n    }\n\n    /**\n     * Extract the range to display on the view, and filter\n     * them according they should be visible or not (attribute 'invisible')\n     *\n     * @private\n     * @param {Element} colNode - the node of 'col' in grid view arch definition\n     * @returns {\n     *      Object<{\n     *          ranges: {\n     *              name: {name: string, label: string, span: string, step: string, hotkey?: string}\n     *          },\n     *          activeRangeName: string,\n     *      }>\n     *  } list of ranges to apply in the grid view.\n     */\n    _extractRanges(colNode) {\n        const ranges = {};\n        let activeRangeName;\n        let firstRangeName = \"\";\n        for (const rangeNode of colNode.children) {\n            const rangeName = rangeNode.getAttribute(\"name\");\n            if (!firstRangeName.length) {\n                firstRangeName = rangeName;\n            }\n            ranges[rangeName] = {\n                name: rangeName,\n                description: rangeNode.getAttribute(\"string\"),\n                span: rangeNode.getAttribute(\"span\"),\n                step: rangeNode.getAttribute(\"step\"),\n                hotkey: rangeNode.getAttribute(\"hotkey\"),\n                default: exprToBoolean(rangeNode.getAttribute(\"default\")),\n            };\n            if (ranges[rangeName].default) {\n                activeRangeName = rangeName;\n            }\n        }\n        return { ranges: ranges, activeRangeName: activeRangeName || firstRangeName };\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { serializeDate, deserializeDate } from \"@web/core/l10n/dates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { Component, useState, onWillUnmount, useRef } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\nexport class GridController extends Component {\n    static components = {\n        Layout,\n        Dropdown,\n        DropdownItem,\n        ViewButton,\n        CogMenu,\n        SearchBar,\n    };\n\n    static props = {\n        ...standardViewProps,\n        archInfo: Object,\n        buttonTemplate: String,\n        Model: Function,\n        Renderer: Function,\n    };\n\n    static template = \"web_grid.GridView\";\n\n    setup() {\n        const state = this.props.state || {};\n        let activeRangeName = this.props.archInfo.activeRangeName;\n        let defaultAnchor;\n        if (state.activeRangeName) {\n            activeRangeName = state.activeRangeName;\n        } else if (this.isMobile && \"day\" in this.props.archInfo.ranges) {\n            activeRangeName = \"day\";\n        }\n        if (state.anchor) {\n            defaultAnchor = state.anchor;\n        } else if (this.props.context.grid_anchor) {\n            defaultAnchor = deserializeDate(this.props.context.grid_anchor);\n        }\n        this.dialogService = useService(\"dialog\");\n        this.model = useModelWithSampleData(this.props.Model, {\n            resModel: this.props.resModel,\n            sectionField: this.props.archInfo.sectionField,\n            rowFields: this.props.archInfo.rowFields,\n            columnFieldName: this.props.archInfo.columnFieldName,\n            measureField: this.props.archInfo.measureField,\n            readonlyField: this.props.archInfo.readonlyField,\n            fieldsInfo: this.props.relatedModels[this.props.resModel].fields,\n            activeRangeName,\n            ranges: this.props.archInfo.ranges,\n            defaultAnchor,\n        });\n        const rootRef = useRef(\"root\");\n        useSetupAction({\n            rootRef: rootRef,\n            getLocalState: () => {\n                const { anchor, range } = this.model.navigationInfo;\n                return {\n                    anchor,\n                    activeRangeName: range?.name,\n                };\n            }\n        })\n        const isWeekendVisible = browser.localStorage.getItem(\"grid.isWeekendVisible\");\n        this.state = useState({\n            activeRangeName: this.model.navigationInfo.range?.name,\n            isWeekendVisible: isWeekendVisible !== null && isWeekendVisible !== undefined\n                ? JSON.parse(isWeekendVisible)\n                : true,\n        });\n        useViewButtons(rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: this.reload.bind(this),\n        });\n        onWillUnmount(() => this.closeDialog?.());\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    get isMobile() {\n        return this.env.isSmall;\n    }\n\n    get isEditable() {\n        return (\n            this.props.archInfo.activeActions.edit &&\n            this.props.archInfo.editable\n        );\n    }\n\n    get displayNoContent() {\n        return (\n            !(this.props.archInfo.displayEmpty || this.model.hasData()) || this.model.useSampleModel\n        );\n    }\n\n    get displayAddALine() {\n        return this.props.archInfo.activeActions.create;\n    }\n\n    get hasDisplayableData() {\n        return true;\n    }\n\n    get options() {\n        const { hideLineTotal, hideColumnTotal, hasBarChartTotal, createInline } =\n            this.props.archInfo;\n        return {\n            hideLineTotal,\n            hideColumnTotal,\n            hasBarChartTotal,\n            createInline,\n        };\n    }\n\n    createRecord(params) {\n        const columnContext = this.model.columnFieldIsDate\n            ? {\n                  [`default_${this.model.columnFieldName}`]: serializeDate(\n                      this.model.navigationInfo.anchor\n                  ),\n              }\n            : {};\n        const context = {\n            ...this.props.context,\n            ...columnContext,\n            ...(params?.context || {}),\n        };\n        this.closeDialog = this.dialogService.add(\n            FormViewDialog,\n            {\n                title: _t(\"New Record\"),\n                resModel: this.model.resModel,\n                viewId: this.props.archInfo.formViewId,\n                onRecordSaved: this.onRecordSaved.bind(this),\n                ...(params || {}),\n                context,\n            },\n            {\n                onClose: () => {\n                    this.closeDialog = null;\n                },\n            }\n        );\n    }\n\n    async beforeExecuteActionButton() {}\n\n    async afterExecuteActionButton() {}\n\n    async reload() {\n        await this.model.fetchData();\n    }\n\n    async onRecordSaved(record) {\n        await this.reload();\n    }\n\n    get columns() {\n        return this.state.isWeekendVisible || this.state.activeRangeName === \"day\" ? this.model.columnsArray : this.model.columnsArray.filter(column => {\n            return DateTime.fromISO(column.value).weekday < 6;\n        });\n    }\n\n    toggleWeekendVisibility() {\n        this.state.isWeekendVisible = !this.state.isWeekendVisible;\n        browser.localStorage.setItem(\"grid.isWeekendVisible\", this.state.isWeekendVisible);\n    }\n}\n", "/** @odoo-module */\n\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from \"@web/core/domain\";\nimport { serializeDate } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Model } from \"@web/model/model\";\nimport { browser } from \"@web/core/browser/browser\";\n\nconst { DateTime, Interval } = luxon;\n\nexport class GridCell {\n    /**\n     * Constructor\n     *\n     * @param dataPoint{GridDataPoint} the grid model.\n     * @param row {GridRow} the grid row linked to the cell.\n     * @param column {GridColumn} the grid column linked to the cell.\n     * @param value {Number} the value of the cell.\n     * @param isHovered {Boolean} is the cell in a hover state?\n     */\n    constructor(dataPoint, row, column, value = 0, isHovered = false) {\n        this._dataPoint = dataPoint;\n        this.row = row;\n        this.column = column;\n        this.model = dataPoint.model;\n        this.value = value;\n        this.isHovered = isHovered;\n        this._readonly = false;\n        this.column.addCell(this);\n    }\n\n    get readonly() {\n        return this._readonly || this.column.readonly;\n    }\n\n    /**\n     * Get the domain of the cell, it will be the domain of row AND the one of the column associated\n     *\n     * @return {Domain} the domain of the cell\n     */\n    get domain() {\n        const domains = [this._dataPoint.searchParams.domain, this.row.domain, this.column.domain];\n        return Domain.and(domains);\n    }\n\n    /**\n     * Get the context to get the default values\n     */\n    get context() {\n        return {\n            ...(this.model.searchParams.context || {}),\n            ...this.row.section?.context,\n            ...this.row.context,\n            ...this.column.context,\n        };\n    }\n\n    get title() {\n        const rowTitle = !this.row.section || this.row.section.isFake\n            ? this.row.title\n            : `${this.row.section.title} / ${this.row.title}`;\n        const columnTitle = this.column.title;\n        return `${rowTitle} (${columnTitle})`;\n    }\n\n    /**\n     * Update the grid cell according to the value set by the current user.\n     *\n     * @param {Number} value the value entered by the current user.\n     */\n    async update(value) {\n        return this.model.mutex.exec(async () => {\n            await this._update(value);\n        });\n    }\n\n    async _update(value) {\n        const oldValue = this.value;\n        const result = await this.model.orm.call(\n            this.model.resModel,\n            \"grid_update_cell\",\n            [this.domain.toList({}), this.model.measureFieldName, value - oldValue],\n            { context: this.context }\n        );\n        if (result) {\n            this.model.actionService.doAction(result);\n            return;\n        }\n        this.row.updateCell(this.column, value);\n        this.model.notify();\n    }\n}\n\nexport class GridRow {\n    /**\n     * Constructor\n     *\n     * @param domain {Domain} the domain of the row.\n     * @param valuePerFieldName {{string: string}} the list of to display the label of the row.\n     * @param dataPoint {GridDataPoint} the grid model.\n     * @param section {GridSection} the section of the grid.\n     * @param columns {GridColumn[]} the columns of the grid.\n     */\n    constructor(domain, valuePerFieldName, dataPoint, section, isAdditionalRow = false) {\n        this._domain = domain;\n        this._dataPoint = dataPoint;\n        this.cells = {};\n        this.valuePerFieldName = valuePerFieldName;\n        this.id = dataPoint.rowId++;\n        this.model = dataPoint.model;\n        this.section = section;\n        if (section) {\n            this.section.addRow(this);\n        }\n        this.grandTotal = 0;\n        this.grandTotalWeekendHidden = 0;\n        this.isAdditionalRow = isAdditionalRow;\n        this._generateCells();\n    }\n\n    get initialRecordValues() {\n        return this.valuePerFieldName;\n    }\n\n    get title() {\n        const labelArray = [];\n        for (const rowField of this._dataPoint.rowFields) {\n            let title = this.valuePerFieldName[rowField.name];\n            if (this.model.fieldsInfo[rowField.name].type === \"many2one\") {\n                if (title) {\n                    title = title[1];\n                } else if (labelArray.length) {\n                    title = \"\";\n                } else {\n                    title = \"None\";\n                }\n            }\n            if (title) {\n                labelArray.push(title);\n            }\n        }\n        return labelArray.join(\" / \");\n    }\n\n    get domain() {\n        if (this.section.isFake) {\n            return this._domain;\n        }\n        return Domain.and([this.section.domain, this._domain]);\n    }\n\n    get context() {\n        const context = {};\n        const getValue = (fieldName, value) =>\n            this.model.fieldsInfo[fieldName].type === \"many2one\" ? value && value[0] : value;\n        for (const [key, value] of Object.entries(this.valuePerFieldName)) {\n            context[`default_${key}`] = getValue(key, value);\n        }\n        return context;\n    }\n\n    getSection() {\n        return !this.section.isFake && this.section;\n    }\n\n    /**\n     * Generate the cells for each column that is present in the row.\n     * @private\n     */\n    _generateCells() {\n        for (const column of this._dataPoint.columnsArray) {\n            this.cells[column.id] = new this.model.constructor.Cell(\n                this._dataPoint,\n                this,\n                column,\n                0\n            );\n        }\n    }\n\n    _ensureColumnExist(column) {\n        if (!(column.id in this._dataPoint.data.columns)) {\n            throw new Error(\"Unbound index: the columnId is not in the row columns\");\n        }\n        return true;\n    }\n\n    /**\n     * Update the cell value of a cell.\n     * @param {GridColumn} column containing the cell to update.\n     * @param {number} value the value to update\n     */\n    updateCell(column, value) {\n        this._ensureColumnExist(column);\n        const cell = this.cells[column.id];\n        const oldValue = cell.value;\n        cell.value = value;\n        const delta = value - oldValue;\n        this.section.updateGrandTotal(column, delta);\n        this.grandTotal += delta;\n        this.grandTotalWeekendHidden += column.isWeekDay ? delta : 0;\n        column.grandTotal += delta;\n        if (this.isAdditionalRow && delta > 0) {\n            this.isAdditionalRow = false;\n        }\n    }\n\n    setReadonlyCell(column, readonly) {\n        this._ensureColumnExist(column);\n        if (readonly instanceof Array) {\n            readonly = readonly.length > 0;\n        } else if (!(readonly instanceof Boolean)) {\n            readonly = Boolean(readonly);\n        }\n        this.cells[column.id]._readonly = readonly;\n    }\n\n    getGrandTotal(showWeekend) {\n        return showWeekend ? this.grandTotal : this.grandTotalWeekendHidden;\n    }\n}\n\nexport class GridSection extends GridRow {\n    constructor() {\n        super(...arguments);\n        this.sectionId = this._dataPoint.sectionId++;\n        this.rows = {};\n        this.isSection = true;\n        this.lastRow = null;\n    }\n\n    get value() {\n        return this.valuePerFieldName && this.valuePerFieldName[this._dataPoint.sectionField.name];\n    }\n\n    get domain() {\n        let value = this.value;\n        if (this.model.fieldsInfo[this._dataPoint.sectionField.name].type === \"many2one\") {\n            value = value && value[0];\n        }\n        return new Domain([[this._dataPoint.sectionField.name, \"=\", value]]);\n    }\n\n    get title() {\n        let title = this.value;\n        if (\n            this._dataPoint.sectionField &&\n            this._dataPoint.fieldsInfo[this._dataPoint.sectionField.name].type === \"many2one\"\n        ) {\n            title = (title && title[1]) || \"None\";\n        }\n        return title;\n    }\n\n    get initialRecordValues() {\n        return { [this._dataPoint.sectionField.name]: this.value };\n    }\n\n    get isFake() {\n        return this.value == null;\n    }\n\n    get context() {\n        const context = {};\n        const getValue = (fieldName, value) =>\n            this.model.fieldsInfo[fieldName].type === \"many2one\" ? value && value[0] : value;\n\n        if (!this.isFake) {\n            const sectionFieldName = this._dataPoint.sectionField.name;\n            context[`default_${sectionFieldName}`] = getValue(sectionFieldName, this.value);\n        }\n        return context;\n    }\n\n    getSection() {\n        return !this.isFake && this;\n    }\n\n    /**\n     * Add row to the section rows.\n     * @param row {GridRow} the row to add.\n     */\n    addRow(row) {\n        if (row.id in this.rows) {\n            throw new Error(\"Row already added in section\");\n        }\n        this.rows[row.id] = row;\n        this.lastRow = row;\n    }\n\n    /**\n     * Update the grand totals according to the provided column and delta.\n     * @param column {GridColumn} the column the grand total has to be updated for.\n     * @param delta {Number} the delta to apply on the grand totals.\n     */\n    updateGrandTotal(column, delta) {\n        this.cells[column.id].value += delta;\n        this.grandTotal += delta;\n        this.grandTotalWeekendHidden += column.isWeekDay ? delta : 0;\n    }\n}\n\nexport class GridColumn {\n    /**\n     * Constructor\n     *\n     * @param dataPoint {GridDataPoint} dataPoint of the grid.\n     * @param title {string} the title of the column to display.\n     */\n    constructor(dataPoint, title, value, readonly = false) {\n        this._dataPoint = dataPoint;\n        this.model = dataPoint.model;\n        this.title = title;\n        this.value = value;\n        this.cells = [];\n        this.id = dataPoint.columnId++;\n        this.grandTotal = 0;\n        this.readonly = readonly;\n    }\n\n    /**\n     * Add the cell to the column cells.\n     * @param cell {GridCell} the cell to add.\n     */\n    addCell(cell) {\n        if (cell.id in this.cells) {\n            throw new Error(\"Cell already added in column\");\n        }\n        this.cells.push(cell);\n        this.grandTotal += cell.value;\n    }\n\n    get domain() {\n        return new Domain([[this._dataPoint.columnFieldName, \"=\", this.value]]);\n    }\n\n    get context() {\n        return { [`default_${this._dataPoint.columnFieldName}`]: this.value };\n    }\n}\n\nexport class DateGridColumn extends GridColumn {\n    /**\n     * Constructor\n     *\n     * @param dataPoint {GridDataPoint} data point of the grid.\n     * @param title {string} the title of the column to display.\n     * @param dateStart {String} the date start serialized\n     * @param dateEnd {String} the date end serialized\n     * @param isToday {Boolean} is the date column representing today?\n     */\n    constructor(dataPoint, title, dateStart, dateEnd, isToday, isWeekDay, readonly = false) {\n        super(dataPoint, title, dateStart, readonly);\n        this.dateEnd = dateEnd;\n        this.isToday = isToday;\n        this.isWeekDay = isWeekDay;\n    }\n\n    get domain() {\n        return new Domain([\n            \"&\",\n            [this._dataPoint.columnFieldName, \">=\", this.value],\n            [this._dataPoint.columnFieldName, \"<\", this.dateEnd],\n        ]);\n    }\n}\n\nexport class GridDataPoint {\n    constructor(model, params) {\n        this.model = model;\n        const { rowFields, sectionField, searchParams } = params;\n        this.rowFields = rowFields;\n        this.sectionField = sectionField;\n        this.searchParams = searchParams;\n        this.sectionId = 0;\n        this.rowId = 0;\n        this.columnId = 0;\n    }\n\n    get orm() {\n        return this.model.orm;\n    }\n\n    get Section() {\n        return this.model.constructor.Section;\n    }\n\n    get Row() {\n        return this.model.constructor.Row;\n    }\n\n    get Column() {\n        return this.model.constructor.Column;\n    }\n\n    get DateColumn() {\n        return this.model.constructor.DateColumn;\n    }\n\n    get Cell() {\n        return this.model.constructor.Cell;\n    }\n\n    get fieldsInfo() {\n        return this.model.fieldsInfo;\n    }\n\n    get columnFieldName() {\n        return this.model.columnFieldName;\n    }\n\n    get resModel() {\n        return this.model.resModel;\n    }\n\n    get fields() {\n        return this._getFields();\n    }\n\n    get groupByFields() {\n        return this._getFields(true);\n    }\n\n    get navigationInfo() {\n        return this.model.navigationInfo;\n    }\n\n    get dateFormat() {\n        return { day: \"ccc,\\nMMM\\u00A0d\", month: \"MMMM\\nyyyy\" };\n    }\n\n    get columnFieldIsDate() {\n        return this.model.columnFieldIsDate;\n    }\n\n    get columnGroupByFieldName() {\n        let columnGroupByFieldName = this.columnFieldName;\n        if (this.columnFieldIsDate) {\n            columnGroupByFieldName += `:${this.navigationInfo.range.step}`;\n        }\n        return columnGroupByFieldName;\n    }\n\n    get readonlyField() {\n        return this.model.readonlyField;\n    }\n\n    get sectionsArray() {\n        return Object.values(this.data.sections);\n    }\n\n    get rowsArray() {\n        return Object.values(this.data.rows);\n    }\n\n    get columnsArray() {\n        return Object.values(this.data.columns);\n    }\n\n    /**\n     * Get fields to use in the group by or in fields of the read_group\n     * @private\n     * @param grouped true to return the fields for the group by.\n     * @return {string[]} list of fields name.\n     */\n    _getFields(grouped = false) {\n        const fields = [];\n        if (!grouped) {\n            fields.push(\n                this.columnFieldName,\n                this.model.measureGroupByFieldName,\n                \"ids:array_agg(id)\"\n            );\n            if (this.readonlyField) {\n                const aggReadonlyField = `${this.readonlyField.name}:${this.readonlyField.aggregator}`;\n                fields.push(aggReadonlyField);\n            }\n        } else {\n            fields.push(this.columnGroupByFieldName);\n        }\n        fields.push(...this.rowFields.map((r) => r.name));\n        if (this.sectionField) {\n            fields.push(this.sectionField.name);\n        }\n        return fields;\n    }\n\n    _getDateColumnTitle(date) {\n        if (this.navigationInfo.range.step in this.dateFormat) {\n            return date.toFormat(this.dateFormat[this.navigationInfo.range.step]);\n        }\n        return serializeDate(date);\n    }\n\n    /**\n     * Generate the date columns.\n     * @private\n     * @return {GridColumn[]}\n     */\n    _generateDateColumns() {\n        const generateNext = (dateStart) =>\n            dateStart.plus({ [`${this.navigationInfo.range.step}s`]: 1 });\n        for (\n            let currentDate = this.navigationInfo.periodStart;\n            currentDate < this.navigationInfo.periodEnd;\n            currentDate = generateNext(currentDate)\n        ) {\n            const domainStart = currentDate;\n            const domainStop = generateNext(currentDate);\n            const domainStartSerialized = serializeDate(domainStart);\n            const isWeekDay = currentDate.weekday < 6;\n            const column = new this.DateColumn(\n                this,\n                this._getDateColumnTitle(currentDate),\n                domainStartSerialized,\n                serializeDate(domainStop),\n                currentDate.startOf(\"day\").equals(this.model.today.startOf(\"day\")),\n                isWeekDay,\n            );\n            this.data.columns[column.id] = column;\n            this.data.columnsKeyToIdMapping[domainStartSerialized] = column.id;\n        }\n    }\n\n    /**\n     * Search grid columns\n     *\n     * @param {Array} domain domain to filter the result\n     * @param {string} readonlyField field uses to make column readonly if true\n     * @returns {Array} array containing id, display_name and readonly if readonlyField is defined.\n     */\n    async _searchMany2oneColumns(domain, readonlyField) {\n        const fieldsToFetch = [\"id\", \"display_name\"];\n        if (readonlyField) {\n            fieldsToFetch.push(readonlyField);\n        }\n        const columnField = this.fieldsInfo[this.columnFieldName];\n        const columnRecords = await this.orm.searchRead(\n            columnField.relation,\n            domain || [],\n            fieldsToFetch\n        );\n        return columnRecords.map((read) => Object.values(read));\n    }\n\n    /**\n     * Initialize the data.\n     * @private\n     */\n    async _initialiseData() {\n        this.data = {\n            columnsKeyToIdMapping: {},\n            columns: {},\n            rows: {},\n            rowsKeyToIdMapping: {},\n            fieldsInfo: this.fieldsInfo,\n            sections: {},\n            sectionsKeyToIdMapping: {},\n        };\n        this.record = {\n            context: {},\n            resModel: this.resModel,\n            resIds: [],\n        };\n        let columnRecords = [];\n        const columnField = this.fieldsInfo[this.columnFieldName];\n        if (this.columnFieldIsDate) {\n            this._generateDateColumns();\n        } else {\n            if (columnField.type === \"selection\") {\n                const selectionFieldValues = await this.orm.call(\n                    \"ir.model.fields\",\n                    \"get_field_selection\",\n                    [this.resModel, this.columnFieldName]\n                );\n                columnRecords = selectionFieldValues;\n            } else if (columnField.type === \"many2one\") {\n                columnRecords = await this._searchMany2oneColumns();\n            } else {\n                throw new Error(\n                    \"Unmanaged column type. Supported types are date, selection and many2one.\"\n                );\n            }\n            for (const record of columnRecords) {\n                let readonly = false;\n                let key, value;\n                if (record.length === 2) {\n                    [key, value] = record;\n                } else {\n                    [key, value, readonly] = record;\n                }\n                const column = new this.Column(this, value, key, Boolean(readonly));\n                this.data.columns[column.id] = column;\n                this.data.columnsKeyToIdMapping[key] = column.id;\n            }\n        }\n    }\n\n    async fetchData() {\n        const data = await this.orm.webReadGroup(\n            this.resModel,\n            Domain.and([this.searchParams.domain, this.model.generateNavigationDomain()]).toList(\n                {}\n            ),\n            this.fields,\n            this.groupByFields,\n            {\n                lazy: false,\n            }\n        );\n        if (this.orm.isSample) {\n            data.groups = data.groups.filter((group) => {\n                const date = DateTime.fromISO(group[\"__range\"][this.columnGroupByFieldName].from);\n                return (\n                    date >= this.navigationInfo.periodStart && date <= this.navigationInfo.periodEnd\n                );\n            });\n        }\n        return data;\n    }\n\n    /**\n     * Gets additional groups to be added to the grid. The call to this function is made in parallel to the main data\n     * fetching.\n     *\n     * This function is intended to be overriden in modules where we want to display additional sections and/or rows in\n     * the grid than what would be returned by the webReadGroup.\n     * The model `sectionField` and `rowFields` can be used in order to know what need to be returned.\n     *\n     * An example of this is:\n     * - when considering timesheet, we want to ease their encoding by adding (to the data that is fetched for scale),\n     *   the entries that have been entered the week before. That way, the first day of week\n     *   (or month, depending on the scale), a line is already displayed with 0's and can directly been used in the\n     *   grid instead of having to use the create button.\n     *\n     * @return {Array<Promise<Object>>} an array of Promise of Object of type:\n     *                                      {\n     *                                          sectionKey: {\n     *                                              value: Any,\n     *                                              rows: {\n     *                                                  rowKey: {\n     *                                                      domain: Domain,\n     *                                                      values: [Any],\n     *                                                  },\n     *                                              },\n     *                                          },\n     *                                      }\n     * @private\n     */\n    _fetchAdditionalData() {\n        return [];\n    }\n\n    /**\n     * Gets additional groups to be added to the grid. The call to this function is made after the main data fetching\n     * has been processed which allows using `data` in the code.\n     * This function is intended to be overriden in modules where we want to display additional sections and/or rows in\n     * the grid than what would be returned by the webReadGroup.\n     * The model `sectionField`, `rowFields` as well as `data` can be used in order to know what need to be returned.\n     *\n     * @return {Array<Promise<Object>>} an array of Promise of Object of type:\n     *                                      {\n     *                                          sectionKey: {\n     *                                              value: Any,\n     *                                              rows: {\n     *                                                  rowKey: {\n     *                                                      domain: Domain,\n     *                                                      values: [Any],\n     *                                                  },\n     *                                              },\n     *                                          },\n     *                                      }\n     * @private\n     */\n    _postFetchAdditionalData() {\n        return [];\n    }\n\n    _getAdditionalPromises() {\n        return [this._fetchUnavailabilityDays()];\n    }\n\n    async _fetchUnavailabilityDays(args = {}) {\n        if (!this.columnFieldIsDate) {\n            return {};\n        }\n        const result = await this.orm.call(\n            this.resModel,\n            \"grid_unavailability\",\n            [\n                serializeDate(this.navigationInfo.periodStart),\n                serializeDate(this.navigationInfo.periodEnd),\n            ],\n            {\n                ...args,\n            }\n        );\n        this._processUnavailabilityDays(result);\n    }\n\n    _processUnavailabilityDays(result) {\n        return;\n    }\n\n    /**\n     * Generate the row key according to the provided read group result.\n     * @param readGroupResult {Array} the read group result the key has to be generated for.\n     * @private\n     * @return {string}\n     */\n    _generateRowKey(readGroupResult) {\n        let key = \"\";\n        const sectionKey =\n            (this.sectionField && this._generateSectionKey(readGroupResult)) || false;\n        for (const rowField of this.rowFields) {\n            let value = rowField.name in readGroupResult && readGroupResult[rowField.name];\n            if (this.fieldsInfo[rowField.name].type === \"many2one\") {\n                value = value && value[0];\n            }\n            key += `${value}\\\\|/`;\n        }\n        return `${sectionKey}@|@${key}`;\n    }\n\n    /**\n     * Generate the section\n     * @param readGroupResult\n     * @private\n     */\n    _generateSectionKey(readGroupResult) {\n        let value = readGroupResult[this.sectionField.name];\n        if (this.fieldsInfo[this.sectionField.name].type === \"many2one\") {\n            value = value && value[0];\n        }\n        return `/|\\\\${value.toString()}`;\n    }\n\n    /**\n     * Generate the row domain for the provided read group result.\n     * @param readGroupResult {Array} the read group result the domain has to be generated for.\n     * @return {{domain: Domain, values: Object}} the generated domain and values.\n     */\n    _generateRowDomainAndValues(readGroupResult) {\n        let domain = new Domain();\n        const values = {};\n        for (const rowField of this.rowFields) {\n            const result = rowField.name in readGroupResult && readGroupResult[rowField.name];\n            let value = result;\n            if (this.fieldsInfo[rowField.name].type === \"many2one\") {\n                value = value && value[0];\n            }\n            values[rowField.name] = result;\n            domain = Domain.and([domain, [[rowField.name, \"=\", value]]]);\n        }\n        return { domain, values };\n    }\n\n    _generateFakeSection() {\n        const section = new this.Section(null, null, this, null);\n        this.data.sections[section.id] = section;\n        this.data.sectionsKeyToIdMapping[\"false\"] = section.id;\n        this.data.rows[section.id] = section;\n        this.data.rowsKeyToIdMapping[\"false\"] = section.id;\n        return section;\n    }\n\n    async _generateData(readGroupResults) {\n        let section;\n        for (const readGroupResult of readGroupResults.groups) {\n            if (!this.orm.isSample) {\n                this.record.resIds.push(...readGroupResult.ids);\n            }\n            const rowKey = this._generateRowKey(readGroupResult);\n            if (this.sectionField) {\n                const sectionKey = this._generateSectionKey(readGroupResult);\n                if (!(sectionKey in this.data.sectionsKeyToIdMapping)) {\n                    const newSection = new this.Section(\n                        null,\n                        { [this.sectionField.name]: readGroupResult[this.sectionField.name] },\n                        this,\n                        null\n                    );\n                    this.data.sections[newSection.id] = newSection;\n                    this.data.sectionsKeyToIdMapping[sectionKey] = newSection.id;\n                    this.data.rows[newSection.id] = newSection;\n                    this.data.rowsKeyToIdMapping[sectionKey] = newSection.id;\n                }\n                section = this.data.sections[this.data.sectionsKeyToIdMapping[sectionKey]];\n            } else if (Object.keys(this.data.sections).length === 0) {\n                section = this._generateFakeSection();\n            }\n            let row;\n            if (!(rowKey in this.data.rowsKeyToIdMapping)) {\n                const { domain, values } = this._generateRowDomainAndValues(readGroupResult);\n                row = new this.Row(domain, values, this, section);\n                this.data.rows[row.id] = row;\n                this.data.rowsKeyToIdMapping[rowKey] = row.id;\n            } else {\n                row = this.data.rows[this.data.rowsKeyToIdMapping[rowKey]];\n            }\n            let columnKey;\n            if (this.columnFieldIsDate) {\n                columnKey = readGroupResult[\"__range\"][this.columnGroupByFieldName].from;\n            } else {\n                const columnField = this.fieldsInfo[this.columnFieldName];\n                if (columnField.type === \"selection\") {\n                    columnKey = readGroupResult[this.columnFieldName];\n                } else if (columnField.type === \"many2one\") {\n                    columnKey = readGroupResult[this.columnFieldName][0];\n                } else {\n                    throw new Error(\n                        \"Unmanaged column type. Supported types are date, selection and many2one.\"\n                    );\n                }\n            }\n            if (this.data.columnsKeyToIdMapping[columnKey] in this.data.columns) {\n                const column = this.data.columns[this.data.columnsKeyToIdMapping[columnKey]];\n                row.updateCell(column, readGroupResult[this.model.measureFieldName]);\n                if (this.readonlyField && this.readonlyField.name in readGroupResult) {\n                    row.setReadonlyCell(column, readGroupResult[this.readonlyField.name]);\n                }\n            }\n        }\n    }\n\n    /**\n     * Method meant to be overridden whenever an item (row and section) post process is needed.\n     * @param item {GridSection|GridRow}\n     */\n    _itemsPostProcess(item) {}\n\n    async load() {\n        await this._initialiseData();\n\n        const mergeAdditionalData = (fetchedData) => {\n            const additionalData = {};\n            for (const data of fetchedData) {\n                for (const [sectionKey, sectionInfo] of Object.entries(data)) {\n                    if (!(sectionKey in additionalData)) {\n                        additionalData[sectionKey] = sectionInfo;\n                    } else {\n                        for (const [rowKey, rowInfo] of Object.entries(sectionInfo.rows)) {\n                            if (!(rowKey in additionalData[sectionKey].rows)) {\n                                additionalData[sectionKey].rows[rowKey] = rowInfo;\n                            }\n                        }\n                    }\n                }\n            }\n            return additionalData;\n        };\n\n        const appendAdditionData = (additionalData) => {\n            for (const [sectionKey, sectionInfo] of Object.entries(additionalData)) {\n                if (!(sectionKey in this.data.sectionsKeyToIdMapping)) {\n                    if (this.sectionField) {\n                        const newSection = new this.Section(\n                            null,\n                            { [this.sectionField.name]: sectionInfo.value },\n                            this,\n                            null\n                        );\n                        this.data.sections[newSection.id] = newSection;\n                        this.data.sectionsKeyToIdMapping[sectionKey] = newSection.id;\n                        this.data.rows[newSection.id] = newSection;\n                        this.data.rowsKeyToIdMapping[sectionKey] = newSection.id;\n                    } else {\n                        // if no sectionField and the section is not in sectionsKeyToIdMapping then no section is generated\n                        this._generateFakeSection();\n                    }\n                }\n                const section = this.data.sections[this.data.sectionsKeyToIdMapping[sectionKey]];\n                for (const [rowKey, rowInfo] of Object.entries(sectionInfo.rows)) {\n                    if (!(rowKey in this.data.rowsKeyToIdMapping)) {\n                        const newRow = new this.Row(\n                            rowInfo.domain,\n                            rowInfo.values,\n                            this,\n                            section,\n                            true\n                        );\n                        this.data.rows[newRow.id] = newRow;\n                        this.data.rowsKeyToIdMapping[rowKey] = newRow.id;\n                        for (const column of Object.values(this.data.columns)) {\n                            newRow.updateCell(column, 0);\n                        }\n                    }\n                }\n            }\n        };\n\n        const [data, additionalData] = await Promise.all([\n            this.fetchData(),\n            Promise.all(this._fetchAdditionalData()),\n        ]);\n        this._generateData(data);\n        appendAdditionData(mergeAdditionalData(additionalData));\n        if (!this.orm.isSample) {\n            const [, postFetchAdditionalData] = await Promise.all([\n                Promise.all(this._getAdditionalPromises()),\n                Promise.all(this._postFetchAdditionalData()),\n            ]);\n            appendAdditionData(mergeAdditionalData(postFetchAdditionalData));\n        }\n\n        this.data.items = [];\n        for (const section of this.sectionsArray) {\n            this.data.items.push(section);\n            this._itemsPostProcess(section);\n            for (const rowId in section.rows) {\n                const row = section.rows[rowId];\n                this._itemsPostProcess(row);\n                this.data.items.push(row);\n            }\n        }\n    }\n}\n\nexport class GridNavigationInfo {\n    constructor(anchor, model) {\n        this.anchor = anchor;\n        this.model = model;\n    }\n\n    get _targetWeekday() {\n        const firstDayOfWeek = localization.weekStart;\n        return this.anchor.weekday < firstDayOfWeek ? firstDayOfWeek - 7 : firstDayOfWeek;\n    }\n\n    get periodStart() {\n        if (this.range.span !== \"week\") {\n            return this.anchor.startOf(this.range.span);\n        }\n        // Luxon's default is monday to monday week so we need to change its behavior.\n        return this.anchor.set({ weekday: this._targetWeekday }).startOf(\"day\");\n    }\n\n    get periodEnd() {\n        if (this.range.span !== \"week\") {\n            return this.anchor.endOf(this.range.span);\n        }\n        // Luxon's default is monday to monday week so we need to change its behavior.\n        return this.anchor\n            .set({ weekday: this._targetWeekday })\n            .plus({ weeks: 1, days: -1 })\n            .endOf(\"day\");\n    }\n\n    get interval() {\n        return Interval.fromDateTimes(this.periodStart, this.periodEnd);\n    }\n\n    contains(date) {\n        return this.interval.contains(date.startOf(\"day\"));\n    }\n}\n\nexport class GridModel extends Model {\n    static DataPoint = GridDataPoint;\n    static Cell = GridCell;\n    static Column = GridColumn;\n    static DateColumn = DateGridColumn;\n    static Row = GridRow;\n    static Section = GridSection;\n    static NavigationInfo = GridNavigationInfo;\n\n    setup(params) {\n        this.notificationService = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.keepLast = new KeepLast();\n        this.mutex = new Mutex();\n        this.defaultSectionField = params.sectionField;\n        this.defaultRowFields = params.rowFields;\n        this.resModel = params.resModel;\n        this.fieldsInfo = params.fieldsInfo;\n        this.columnFieldName = params.columnFieldName;\n        this.columnFieldIsDate = this.fieldsInfo[params.columnFieldName].type === \"date\";\n        this.measureField = params.measureField;\n        this.readonlyField = params.readonlyField;\n        this.ranges = params.ranges;\n        this.defaultAnchor = params.defaultAnchor || this.today;\n        this.navigationInfo = new this.constructor.NavigationInfo(this.defaultAnchor, this);\n        const activeRangeName =\n            browser.localStorage.getItem(this.storageKey) || params.activeRangeName;\n        if (Object.keys(this.ranges).length && activeRangeName) {\n            this.navigationInfo.range = this.ranges[activeRangeName];\n        }\n    }\n\n    get data() {\n        return this._dataPoint?.data || {};\n    }\n\n    get record() {\n        return this._dataPoint?.record || {};\n    }\n\n    get today() {\n        return DateTime.local().startOf(\"day\");\n    }\n\n    get sectionsArray() {\n        return Object.values(this.data.sections);\n    }\n\n    get itemsArray() {\n        return this.data.items;\n    }\n\n    get columnsArray() {\n        return Object.values(this.data.columns);\n    }\n\n    get maxColumnsTotal() {\n        return Math.max(...this.columnsArray.map((c) => c.grandTotal));\n    }\n\n    get measureFieldName() {\n        return this.measureField.name;\n    }\n\n    get measureGroupByFieldName() {\n        if (this.measureField.aggregator) {\n            return `${this.measureFieldName}:${this.measureField.aggregator}`;\n        }\n        return this.measureFieldName;\n    }\n\n    get storageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n\n    isToday(date) {\n        return date.startOf(\"day\").equals(this.today.startOf(\"day\"));\n    }\n\n    /**\n     * Set the new range according to the range name passed into parameter.\n     * @param rangeName {string} the range name to set.\n     */\n    async setRange(rangeName) {\n        this.navigationInfo.range = this.ranges[rangeName];\n        browser.localStorage.setItem(this.storageKey, rangeName);\n        await this.fetchData();\n    }\n\n    async setAnchor(anchor) {\n        this.navigationInfo.anchor = anchor;\n        await this.fetchData();\n    }\n\n    async setTodayAnchor() {\n        await this.setAnchor(this.today);\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.sectionsArray.length;\n    }\n\n    generateNavigationDomain() {\n        if (this.columnFieldIsDate) {\n            return new Domain([\n                \"&\",\n                [this.columnFieldName, \">=\", serializeDate(this.navigationInfo.periodStart)],\n                [this.columnFieldName, \"<=\", serializeDate(this.navigationInfo.periodEnd)],\n            ]);\n        } else {\n            return Domain.TRUE;\n        }\n    }\n\n    /**\n     * Reset the anchor\n     */\n    async resetAnchor() {\n        await this.setAnchor(this.defaultAnchor);\n    }\n\n    /**\n     * Move the anchor to the next/previous step\n     * @param direction {\"forward\"|\"backward\"} the direction to the move the anchor\n     */\n    async moveAnchor(direction) {\n        if (direction == \"forward\") {\n            this.navigationInfo.anchor = this.navigationInfo.anchor.plus({\n                [this.navigationInfo.range.span]: 1,\n            });\n        } else if (direction == \"backward\") {\n            this.navigationInfo.anchor = this.navigationInfo.anchor.minus({\n                [this.navigationInfo.range.span]: 1,\n            });\n        } else {\n            throw Error(\"Invalid argument\");\n        }\n        if (\n            this.navigationInfo.contains(this.today) &&\n            this.navigationInfo.anchor.startOf(\"day\").equals(this.today.startOf(\"day\"))\n        ) {\n            this.navigationInfo.anchor = this.today;\n        }\n        await this.fetchData();\n    }\n\n    /**\n     * Load the model\n     *\n     * @override\n     * @param params {Object} the search parameters (domain, groupBy, etc.)\n     * @return {Promise<void>}\n     */\n    async load(params = {}) {\n        const searchParams = {\n            ...this.searchParams,\n            ...params,\n        };\n        const groupBys = [];\n        let notificationDisplayed = false;\n        for (const groupBy of searchParams.groupBy) {\n            if (groupBy.startsWith(this.columnFieldName)) {\n                if (!notificationDisplayed) {\n                    this.notificationService.add(\n                        _t(\n                            \"Grouping by the field used in the column of the grid view is not possible.\"\n                        ),\n                        { type: \"warning\" }\n                    );\n                    notificationDisplayed = true;\n                }\n            } else {\n                groupBys.push(groupBy);\n            }\n        }\n        if (searchParams.length !== groupBys.length) {\n            searchParams.groupBy = groupBys;\n        }\n        let rowFields = [];\n        let sectionField;\n        if (searchParams.groupBy.length) {\n            if (\n                this.defaultSectionField &&\n                searchParams.groupBy.length > 1 &&\n                searchParams.groupBy[0] === this.defaultSectionField.name\n            ) {\n                sectionField = this.defaultSectionField;\n            }\n            const rowFieldPerFieldName = Object.fromEntries(\n                this.defaultRowFields.map((r) => [r.name, r])\n            );\n            for (const groupBy of searchParams.groupBy) {\n                if (sectionField && groupBy === sectionField.name) {\n                    continue;\n                }\n                if (groupBy in rowFieldPerFieldName) {\n                    rowFields.push({\n                        ...rowFieldPerFieldName[groupBy],\n                        invisible: \"False\",\n                    });\n                } else {\n                    rowFields.push({ name: groupBy });\n                }\n            }\n        } else {\n            if (this.defaultSectionField && (this.defaultSectionField.invisible !== \"True\" && this.defaultSectionField.invisible !== \"1\")) {\n                sectionField = this.defaultSectionField;\n            }\n            rowFields = this.defaultRowFields.filter((r) => (r.invisible !== \"True\" && r.invisible !== \"1\"));\n        }\n\n        const dataPoint = new this.constructor.DataPoint(this, {\n            searchParams,\n            rowFields,\n            sectionField,\n        });\n        await this.keepLast.add(dataPoint.load());\n        this._dataPoint = dataPoint;\n\n        this.searchParams = searchParams;\n        this.rowFields = rowFields;\n        this.sectionField = sectionField;\n    }\n\n    async fetchData(params = {}) {\n        await this.load(params);\n        this.useSampleModel = false;\n        this.notify();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { useVirtualGrid } from \"@web/core/virtual_grid_hook\";\nimport { Field } from \"@web/views/fields/field\";\nimport { Record } from \"@web/model/record\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { ViewScaleSelector } from \"@web/views/view_components/view_scale_selector\";\n\nimport { GridComponent } from \"@web_grid/components/grid_component/grid_component\";\n\nimport {\n    Component,\n    markup,\n    useState,\n    onWillUpdateProps,\n    onMounted,\n    onPatched,\n    reactive,\n    useRef,\n    useExternalListener,\n} from \"@odoo/owl\";\n\nexport class GridRenderer extends Component {\n    static components = {\n        Field,\n        GridComponent,\n        Record,\n        ViewScaleSelector,\n    };\n\n    static template = \"web_grid.Renderer\";\n\n    static props = {\n        sections: { type: Array, optional: true },\n        columns: { type: Array, optional: true },\n        rows: { type: Array, optional: true },\n        model: { type: Object, optional: true },\n        options: Object,\n        sectionField: { type: Object, optional: true },\n        rowFields: Array,\n        measureField: Object,\n        isEditable: Boolean,\n        widgetPerFieldName: Object,\n        openAction: { type: Object, optional: true },\n        contentRef: Object,\n        createInline: Boolean,\n        createRecord: Function,\n        ranges: { type: Object, optional: true },\n        state: Object,\n        toggleWeekendVisibility: Function,\n    };\n\n    static defaultProps = {\n        sections: [],\n        columns: [],\n        rows: [],\n        model: {},\n        ranges: {},\n    };\n\n    setup() {\n        this.rendererRef = useRef(\"renderer\");\n        this.actionService = useService(\"action\");\n        this.editionState = useState({\n            hoveredCellInfo: false,\n            editedCellInfo: false,\n        });\n        this.hoveredElement = null;\n        const measureFieldName = this.props.model.measureFieldName;\n        const fieldInfo = this.props.model.fieldsInfo[measureFieldName];\n        const measureFieldWidget = this.props.widgetPerFieldName[measureFieldName];\n        const widgetName = measureFieldWidget || fieldInfo.type;\n        this.gridCell = registry.category(\"grid_components\").get(widgetName);\n        this.hoveredCellProps = {\n            // props cell hovered\n            name: measureFieldName,\n            type: widgetName,\n            component: this.gridCell.component,\n            reactive: reactive({ cell: null }),\n            fieldInfo,\n            readonly: !this.props.isEditable,\n            openRecords: this.openRecords.bind(this),\n            editMode: false,\n            onEdit: this.onEditCell.bind(this),\n            getCell: this.getCell.bind(this),\n            isMeasure: true,\n        };\n        this.editCellProps = {\n            // props for cell in edit mode\n            name: measureFieldName,\n            type: widgetName,\n            component: this.gridCell.component,\n            reactive: reactive({ cell: null }),\n            fieldInfo,\n            readonly: !this.props.isEditable,\n            openRecords: this.openRecords.bind(this),\n            editMode: true,\n            onEdit: this.onEditCell.bind(this),\n            getCell: this.getCell.bind(this),\n            onKeyDown: this.onCellKeydown.bind(this),\n            isMeasure: true,\n        };\n        this.isEditing = false;\n        onWillUpdateProps(this.onWillUpdateProps);\n        onMounted(this._focusOnToday);\n        onPatched(this._focusOnToday);\n        // This property is used to avoid refocus on today whenever a cell value is updated.\n        this.shouldFocusOnToday = true;\n        this.onMouseOver = useDebounced(this._onMouseOver, 10);\n        this.onMouseOut = useDebounced(this._onMouseOut, 10);\n        this.virtualGrid = useVirtualGrid({\n            scrollableRef: this.props.contentRef,\n            initialScroll: { top: 60 },\n        });\n        useExternalListener(window, \"click\", this.onClick);\n        useExternalListener(window, \"keydown\", this.onKeyDown);\n    }\n\n    getCell(rowId, columnId) {\n        return this.props.model.data.rows[rowId]?.cells[columnId];\n    }\n\n    getItemHeight(item) {\n        let height = this.rowHeight;\n        if (item.isSection && item.isFake) {\n            return 0;\n        }\n        if (this.props.createInline && !item.isSection && item.section.lastRow.id === item.id) {\n            height *= 2; // to include the Add a line row\n        }\n        return height;\n    }\n\n    get isMobile() {\n        return this.env.isSmall;\n    }\n\n    get rowHeight() {\n        const baseHeight = this.isMobile ? 48 : 32;\n        /*\n         * On mobile devices, grouped by fields are stacked vertically.\n         * By default, the base height accommodates up to 2 fields.\n         * For each additional field beyond the first 2, we add 20px to maintain proper spacing.\n         */\n        const extraHeight = this.isMobile\n            ? Math.max(0, this.props.model.rowFields.length - 2) * 20\n            : 0;\n        return baseHeight + extraHeight;\n    }\n\n    get virtualRows() {\n        this.virtualGrid.setRowsHeights(this.props.rows.map((row) => this.getItemHeight(row)));\n        const [start, end] = this.virtualGrid.rowsIndexes;\n        return this.props.rows.slice(start, end + 1);\n    }\n\n    getRowPosition(row, isCreateInlineRow = false) {\n        const rowIndex = row ? this.props.rows.findIndex((r) => r.id === row.id) : 0;\n        const section = row && row.getSection();\n        const sectionDisplayed = Boolean(section && (section.value || this.props.sections.length > 1));\n        let rowPosition = this.rowsGap + rowIndex + 1 + (sectionDisplayed ? section.sectionId : 0);\n        if (isCreateInlineRow) {\n            rowPosition += 1;\n        }\n        if (!sectionDisplayed) {\n            rowPosition -= 1;\n        }\n        return rowPosition;\n    }\n\n    getTotalRowPosition() {\n        let sectionIndex = 0;\n        if (this.props.model.sectionField && this.props.sections.length) {\n            if (this.props.sections.length > 1 || this.props.sections[0].value) {\n                sectionIndex = this.props.sections.length;\n            }\n        }\n        return (\n            (this.props.rows.length || 1) +\n            sectionIndex +\n            (this.props.createInline ? 1 : 0) +\n            this.rowsGap\n        );\n    }\n\n    onWillUpdateProps(nextProps) {}\n\n    formatValue(value) {\n        return this.gridCell.formatter(value);\n    }\n\n    /**\n     * @deprecated\n     * TODO: [XBO] remove me in master\n     * @param {*} data\n     */\n    getDefaultState(data) {\n        return {};\n    }\n\n    get rowsCount() {\n        const addLineRows = this.props.createInline ? this.props.sections.length || 1 : 0;\n        return this.props.rows.length - (this.props.model.sectionField ? 0 : 1) + addLineRows;\n    }\n\n    get gridTemplateRows() {\n        let totalRows = 0;\n        if (!this.props.options.hideColumnTotal) {\n            totalRows += 1;\n            if (this.props.options.hasBarChartTotal) {\n                totalRows += 1;\n            }\n        }\n        // Row height must be hard-coded for the virtual hook to work properly.\n        return `auto repeat(${this.rowsCount + totalRows}, ${this.rowHeight}px)`;\n    }\n\n    get gridTemplateColumns() {\n        return `auto repeat(${this.props.columns.length}, ${\n            this.props.columns.length > 7 ? \"minmax(8ch, auto)\" : \"minmax(10ch, 1fr)\"\n        }) minmax(10ch, 10em)`;\n    }\n\n    get measureLabel() {\n        const measureFieldName = this.props.model.measureFieldName;\n        if (measureFieldName === \"__count\") {\n            return _t(\"Total\");\n        }\n        return (\n            this.props.measureField.string || this.props.model.fieldsInfo[measureFieldName].string\n        );\n    }\n\n    get rowsGap() {\n        return 1;\n    }\n\n    get columnsGap() {\n        return 1;\n    }\n\n    get displayAddLine() {\n        return this.props.createInline && this.row.id === this.row.section.lastRow.id;\n    }\n\n    getCellColorClass(column) {\n        return \"text-900\";\n    }\n\n    getSectionColumnsClasses(column, row) {\n        const isToday = column.isToday;\n        return {\n            'bg-info bg-opacity-50': isToday,\n            'bg-200 border-top': !isToday,\n            'bg-opacity-75': this.getUnavailableClass(column) === 'o_grid_unavailable' && row.cells[column.id].value === 0,\n        }\n    }\n\n    getSectionCellsClasses(column, row) {\n        return {\n            'text-opacity-25' : row.cells[column.id].value === 0 || this.getUnavailableClass(column) === 'o_grid_unavailable',\n        };\n    }\n\n    isTextDanger() {\n        return false;\n    }\n\n    getTextColorClasses(column, row, isEven) {\n        const value = row.cells[column.id].value;\n        const isTextDanger = this.isTextDanger(row, column);\n        return {\n            'text-bg-view': isEven && value >= 0 && !isTextDanger,\n            'text-900': !isEven && value >= 0 && !isTextDanger,\n            'text-danger': value < 0 || isTextDanger,\n        }\n    }\n\n    getCellsClasses(column, row, section, isEven) {\n        return {\n            ...this.getTextColorClasses(column, row, isEven),\n            'o_grid_cell_today': column.isToday,\n            'fst-italic': row.isAdditionalRow,\n        };\n    }\n\n    _getSectionTotalCellBgColor(section) {\n        return 'text-bg-800';\n    }\n\n    getSectionTotalRowClass(section, grandTotal) {\n        return {\n            [this._getSectionTotalCellBgColor(section)]: true,\n            'text-opacity-25': grandTotal === 0,\n        };\n    }\n\n    getColumnBarChartHeightStyle(column) {\n        let heightPercentage = 0;\n        if (this.props.model.maxColumnsTotal !== 0) {\n            heightPercentage = (column.grandTotal / this.props.model.maxColumnsTotal) * 100;\n        }\n        return `height: ${heightPercentage}%; bottom: 0;`;\n    }\n\n    getFooterTotalCellClasses(grandTotal) {\n        if (grandTotal < 0) {\n            return \"bg-danger text-bg-danger\";\n        }\n\n        return \"bg-400\";\n    }\n\n    getUnavailableClass(column, section = undefined) {\n        return \"\";\n    }\n\n    getFieldAdditionalProps(fieldName) {\n        return {\n            name: fieldName,\n            type: this.props.widgetPerFieldName[fieldName] || this.props.model.fieldsInfo[fieldName].type,\n        };\n    }\n\n    onCreateInlineClick(section) {\n        const context = {\n            ...(section?.context || {}),\n        };\n        const title = _t(\"Add a Line\");\n        this.props.createRecord({ context, title });\n    }\n\n    _focusOnToday() {\n        if (!this.shouldFocusOnToday) {\n            return;\n        }\n        this.shouldFocusOnToday = false;\n        const { navigationInfo, columnFieldIsDate } = this.props.model;\n        if (this.isMobile || !columnFieldIsDate || navigationInfo.range.name != \"month\"){\n            return;\n        }\n        const rendererEl = this.rendererRef.el;\n        const todayEl = rendererEl.querySelector(\"div.o_grid_column_title.fw-bolder\");\n        if (todayEl) {\n            rendererEl.parentElement.scrollLeft = todayEl.offsetLeft - rendererEl.offsetWidth / 2 + todayEl.offsetWidth / 2;\n        }\n    }\n\n    _onMouseOver(ev) {\n        if (this.hoveredElement || ev.fromElement?.classList.contains(\"dropdown-item\")) {\n            // As mouseout is call prior to mouseover, if hoveredElement is set this means\n            // that we haven't left it. So it's a mouseover inside it.\n            return;\n        }\n        const highlightableElement = ev.target.closest(\".o_grid_highlightable\");\n        if (!highlightableElement) {\n            // We are not in an element that should trigger a highlight.\n            return;\n        }\n        const { column, gridRow, gridColumn, row } = highlightableElement.dataset;\n        const isCellInColumnTotalHighlighted =\n            highlightableElement.classList.contains(\"o_grid_row_total\");\n        const elementsToHighlight = this.rendererRef.el.querySelectorAll(\n            `.o_grid_highlightable[data-grid-row=\"${gridRow}\"]:not(.o_grid_add_line):not(.o_grid_column_title), .o_grid_highlightable[data-grid-column=\"${gridColumn}\"]:not(.o_grid_row_timer):not(.o_grid_section_title):not(.o_grid_row_title${\n                isCellInColumnTotalHighlighted ? \",.o_grid_row_total\" : \"\"\n            })`\n        );\n        for (const node of elementsToHighlight) {\n            if (node.classList.contains(\"o_grid_bar_chart_container\")) {\n                node.classList.add(\"o_grid_highlighted\");\n            }\n            if (node.dataset.gridRow === gridRow) {\n                node.classList.add(\"o_grid_highlighted\");\n                if (node.dataset.gridColumn === gridColumn) {\n                    node.classList.add(\"o_grid_cell_highlighted\");\n                } else {\n                    node.classList.add(\"o_grid_row_highlighted\");\n                }\n            }\n        }\n        this.hoveredElement = highlightableElement;\n        const cell = this.editCellProps.reactive.cell;\n        if (\n            row &&\n            column &&\n            !(cell && cell.dataset.row === row && cell.dataset.column === column)\n        ) {\n            this.hoveredCellProps.reactive.cell = highlightableElement;\n        }\n    }\n\n    /**\n     * Mouse out handler\n     *\n     * @param {MouseEvent} ev\n     */\n    _onMouseOut(ev) {\n        if (!this.hoveredElement) {\n            // If hoveredElement is not set this means were not in a o_grid_highlightable. So ignore it.\n            return;\n        }\n        /** @type {HTMLElement | null} */\n        let relatedTarget = ev.relatedTarget;\n        const gridCell = relatedTarget?.closest(\".o_grid_cell\");\n        if (\n            gridCell &&\n            gridCell.dataset.gridRow === this.hoveredElement.dataset.gridRow &&\n            gridCell.dataset.gridColumn === this.hoveredElement.dataset.gridColumn &&\n            gridCell !== this.editCellProps.reactive.cell\n        ) {\n            return;\n        }\n        while (relatedTarget) {\n            // Go up the parent chain\n            if (relatedTarget === this.hoveredElement) {\n                // Check that we are still inside hoveredConnector.\n                // If so it means it is a transition between child elements so ignore it.\n                return;\n            }\n            relatedTarget = relatedTarget.parentElement;\n        }\n        const { gridRow, gridColumn } = this.hoveredElement.dataset;\n        const elementsHighlighted = this.rendererRef.el.querySelectorAll(\n            `.o_grid_highlightable[data-grid-row=\"${gridRow}\"], .o_grid_highlightable[data-grid-column=\"${gridColumn}\"]`\n        );\n        for (const node of elementsHighlighted) {\n            node.classList.remove(\n                \"o_grid_highlighted\",\n                \"o_grid_row_highlighted\",\n                \"o_grid_cell_highlighted\"\n            );\n        }\n        this.hoveredElement = null;\n        if (this.hoveredCellProps.reactive.cell) {\n            this.hoveredCellProps.reactive.cell\n                .querySelector(\".o_grid_cell_readonly\")\n                .classList.remove(\"d-none\");\n            this.hoveredCellProps.reactive.cell = null;\n        }\n    }\n\n    onEditCell(value) {\n        if (this.editCellProps.reactive.cell) {\n            this.editCellProps.reactive.cell\n                .querySelector(\".o_grid_cell_readonly\")\n                .classList.remove(\"d-none\");\n        }\n        if (value) {\n            this.editCellProps.reactive.cell = this.hoveredCellProps.reactive.cell;\n            this.hoveredCellProps.reactive.cell = null;\n        } else {\n            this.editCellProps.reactive.cell = null;\n        }\n    }\n\n    _onKeyDown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"escape\" && this.editCellProps.reactive.cell) {\n            this.onEditCell(false);\n        }\n    }\n\n    /**\n     * Handle click on any element in the grid\n     *\n     * @param {MouseEvent} ev\n     */\n    onClick(ev) {\n        if (\n            !this.editCellProps.reactive.cell ||\n            ev.target.closest(\".o_grid_highlighted\") ||\n            ev.target.closest(\".o_grid_cell\")\n        ) {\n            return;\n        }\n        this.onEditCell(false);\n    }\n\n    onKeyDown(ev) {\n        this._onKeyDown(ev);\n    }\n\n    /**\n     * Handle the click on a cell in mobile\n     *\n     * @param {MouseEvent} ev\n     */\n    onCellClick(ev) {\n        ev.stopPropagation();\n        const cell = ev.target.closest(\".o_grid_highlightable\");\n        const { row, column } = cell.dataset;\n        if (row && column) {\n            if (this.editCellProps.reactive.cell) {\n                this.editCellProps.reactive.cell\n                    .querySelector(\".o_grid_cell_readonly\")\n                    .classList.remove(\"d-none\");\n            }\n            this.editCellProps.reactive.cell = cell;\n        }\n    }\n\n    /**\n     * Handle keydown when cell is edited in the grid view.\n     *\n     * @param {KeyboardEvent} ev\n     * @param {import(\"./grid_model\").GridCell | null} cell\n     */\n    onCellKeydown(ev, cell) {\n        const hotkey = getActiveHotkey(ev);\n        if (!this.rendererRef.el || !cell || ![\"tab\", \"shift+tab\", \"enter\"].includes(hotkey)) {\n            this._onKeyDown(ev);\n            return;\n        }\n        // Purpose: prevent browser defaults\n        ev.preventDefault();\n        // Purpose: stop other window keydown listeners (e.g. home menu)\n        ev.stopImmediatePropagation();\n        let rowId = cell.row.id;\n        let columnId = cell.column.id;\n        const columnIds = this.props.columns.map((c) => c.id);\n        const rowIds = [];\n        for (const item of this.props.rows) {\n            if (!item.isSection) {\n                rowIds.push(item.id);\n            }\n        }\n        let columnIndex = columnIds.indexOf(columnId);\n        let rowIndex = rowIds.indexOf(rowId);\n        if (hotkey === \"tab\") {\n            columnIndex += 1;\n            rowIndex += 1;\n            if (columnIndex < columnIds.length) {\n                columnId = columnIds[columnIndex];\n            } else {\n                columnId = columnIds[0];\n                if (rowIndex < rowIds.length) {\n                    rowId = rowIds[rowIndex];\n                } else {\n                    rowId = rowIds[0];\n                }\n            }\n        } else if (hotkey === \"shift+tab\") {\n            columnIndex -= 1;\n            rowIndex -= 1;\n            if (columnIndex >= 0) {\n                columnId = columnIds[columnIndex];\n            } else {\n                columnId = columnIds[columnIds.length - 1];\n                if (rowIndex >= 0) {\n                    rowId = rowIds[rowIndex];\n                } else {\n                    rowId = rowIds[rowIds.length - 1];\n                }\n            }\n        } else if (hotkey === \"enter\") {\n            rowIndex += 1;\n            if (rowIndex >= rowIds.length) {\n                columnIndex = (columnIndex + 1) % columnIds.length;\n                columnId = columnIds[columnIndex];\n            }\n            rowIndex = rowIndex % rowIds.length;\n            rowId = rowIds[rowIndex];\n        }\n        this.onEditCell(false);\n        this.hoveredCellProps.reactive.cell = this.rendererRef.el.querySelector(\n            `.o_grid_highlightable[data-row=\"${rowId}\"][data-column=\"${columnId}\"]`\n        );\n        this.onEditCell(true);\n    }\n\n    async openRecords(actionTitle, domain, context) {\n        const resModel = this.props.model.resModel;\n        if (this.props.openAction) {\n            const resIds = await this.props.model.orm.search(resModel, domain);\n            this.actionService.doActionButton({\n                ...this.props.openAction,\n                resModel,\n                resIds,\n                context,\n            });\n        } else {\n            // retrieve form and list view ids from the action\n            const { views = [] } = this.env.config;\n            const openRecordsViews = [\"list\", \"form\"].map((viewType) => {\n                const view = views.find((view) => view[1] === viewType);\n                return [view ? view[0] : false, viewType];\n            });\n            this.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                name: actionTitle,\n                res_model: resModel,\n                views: openRecordsViews,\n                domain,\n                context,\n                help: this._getNoContentHelper(),\n            });\n        }\n    }\n\n    /** Return grid cell action helper when no records are found. */\n    _getNoContentHelper() {\n        const noActivitiesFound = _t(\"No activities found\");\n        return  markup(\n            `<p class='o_view_nocontent_smiling_face'>${escape(noActivitiesFound)}</p>`\n        );\n    }\n\n    onMagnifierGlassClick(section, column) {\n        const title = `${section.title} (${column.title})`;\n        const domain = Domain.and([section.domain, column.domain]).toList();\n        this.openRecords(title, domain, section.context);\n    }\n\n    get rangesArray() {\n        return Object.values(this.props.ranges);\n    }\n\n    async onRangeClick(name) {\n        await this.props.model.setRange(name);\n        this.props.state.activeRangeName = name;\n        this.shouldFocusOnToday = true;\n    }\n\n    async onTodayButtonClick() {\n        await this.props.model.setTodayAnchor();\n        this.shouldFocusOnToday = true;\n    }\n\n    async onPreviousButtonClick() {\n        await this.props.model.moveAnchor(\"backward\");\n        this.shouldFocusOnToday = true;\n    }\n\n    async onNextButtonClick() {\n        await this.props.model.moveAnchor(\"forward\");\n        this.shouldFocusOnToday = true;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { GridArchParser } from \"@web_grid/views/grid_arch_parser\";\nimport { GridController } from \"@web_grid/views/grid_controller\";\nimport { GridModel } from \"@web_grid/views/grid_model\";\nimport { GridRenderer } from \"@web_grid/views/grid_renderer\";\n\nexport const gridView = {\n    type: \"grid\",\n    ArchParser: GridArchParser,\n    Controller: GridController,\n    Model: GridModel,\n    Renderer: GridRenderer,\n    buttonTemplate: \"web_grid.Buttons\",\n\n    props: (genericProps, view) => {\n        const { ArchParser, Model, Renderer, buttonTemplate: viewButtonTemplate } = view;\n        const { arch, relatedModels, resModel, buttonTemplate } = genericProps;\n        return {\n            ...genericProps,\n            archInfo: new ArchParser().parse(arch, relatedModels, resModel),\n            buttonTemplate: buttonTemplate || viewButtonTemplate,\n            Model,\n            Renderer,\n        };\n    }\n};\n\nregistry.category('views').add('grid', gridView);\n", "/** @odoo-module */\n\nimport { serializeDate, deserializeDate } from \"@web/core/l10n/dates\";\nimport { GridNavigationInfo, GridModel, GridDataPoint } from \"@web_grid/views/grid_model\";\n\nexport class AnalyticLineGridDataPoint extends GridDataPoint {\n    async _initialiseData() {\n        if (this.navigationInfo.range.span === \"year\") {\n            await this.navigationInfo.fetchPeriod();\n        }\n        await super._initialiseData();\n    }\n}\n\nexport class AnalyticLineGridNavigationInfo extends GridNavigationInfo {\n    get periodStart() {\n        if (this.range.span !== \"year\" || !this._periodStart) {\n            return super.periodStart;\n        }\n        return this._periodStart;\n    }\n\n    get periodEnd() {\n        if (this.range.span !== \"year\" || !this._periodEnd) {\n            return super.periodEnd;\n        }\n        return this._periodEnd;\n    }\n\n    async fetchPeriod() {\n        const { date_from, date_to } = await this.model.orm.call(\n            this.model.resModel,\n            \"grid_compute_year_range\",\n            [serializeDate(this.anchor)]\n        );\n        this._periodStart = deserializeDate(date_from);\n        this._periodEnd = deserializeDate(date_to);\n    }\n}\n\nexport class AnalyticLineGridModel extends GridModel {\n    static DataPoint = AnalyticLineGridDataPoint;\n    static NavigationInfo = AnalyticLineGridNavigationInfo;\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nimport { gridView } from \"@web_grid/views/grid_view\";\n\nimport { AnalyticLineGridModel } from \"./analytic_line_grid_model\";\n\nexport const analyticLineGridView = {\n    ...gridView,\n    Model: AnalyticLineGridModel,\n}\n\nregistry.category(\"views\").add(\"analytic_line_grid\", analyticLineGridView)\n", "import { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\nimport { AvatarCardEmployeePopover } from \"@hr/components/avatar_card_employee/avatar_card_employee_popover\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\n\nexport class GanttEmployeeAvatar extends Avatar {\n    static template = \"hr.GanttEmployeeAvatar\";\n\n    setup() {\n        super.setup();\n        this.avatarCard = usePopover(AvatarCardEmployeePopover);\n    }\n\n    openCard(ev) {\n        if (this.env.isSmall || !this.props.resId) {\n            return;\n        }\n        const target = ev.currentTarget;\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(target, {\n                id: this.props.resId,\n            });\n        }\n    }\n}\n", "import { GanttEmployeeAvatar } from \"./hr_gantt_employee_avatar\";\nimport { GanttRenderer } from \"@web_gantt/gantt_renderer\";\n\nconst { DateTime } = luxon;\n\nexport class HrGanttRenderer extends GanttRenderer {\n    static rowHeaderTemplate = \"hr.HrGanttRenderer.RowHeader\";\n    static components = { ...GanttRenderer.components, Avatar: GanttEmployeeAvatar };\n    computeDerivedParams() {\n        this.rowsWithAvatar = {};\n        super.computeDerivedParams();\n    }\n\n    getAvatarProps(row) {\n        return this.rowsWithAvatar[row.id];\n    }\n\n    hasAvatar(row) {\n        return row.id in this.rowsWithAvatar;\n    }\n\n    processRow(row) {\n        const { groupedByField, name, resId } = row;\n        if (groupedByField === \"employee_id\" && Boolean(resId)) {\n            const { fields } = this.model.metaData;\n            const relation = fields.employee_id.relation;\n            const resModel = relation === 'hr.employee' ? 'hr.employee.public' : relation;\n            this.rowsWithAvatar[row.id] = { resModel, resId, displayName: name };\n        }\n        return super.processRow(...arguments);\n    }\n\n    /**\n     * Override to factor in lunch brakes between 11:00 to 14:00.\n     * Stops morning or afternoon shift pills from spanning an entire day.\n     *\n     * @param {RelationalRecord} record\n     * @returns {Partial<Pill>}\n     */\n    getPill(record) {\n        const pill = super.getPill(record);\n        const { unit } = this.model.metaData.scale;\n        const [startIndex, endIndex] = pill.grid.column;\n        // only check pills that span 2 half-days for in week or month views\n        if ([\"week\", \"month\"].includes(unit) && (endIndex - startIndex === 2)) {\n            const {\n                dateStartField,\n                dateStopField,\n                globalStart,\n                globalStop,\n            } = this.model.metaData;\n            const start = DateTime.max(globalStart, record[dateStartField]);\n            const stop = DateTime.min(globalStop, record[dateStopField]);\n            if (start.day === stop.day) {\n                const startTime = start.hour + (start.minute / 60);\n                const stopTime = stop.hour + (stop.minute / 60);\n                // we can assume startTime < 12:00 and stopTime > 12:00\n                const closestToNoon = 12 - startTime < stopTime - 12 ? startTime : stopTime;\n                if (startTime >= 11 && startTime === closestToNoon) {\n                    // most of pill is placed in afternoon, so round off first half\n                    pill.grid.column = [startIndex + 1, endIndex];\n                } else if (stopTime <= 14) {\n                    // start time is before 11:00 or most of pill is before noon\n                    // so round off second half\n                    pill.grid.column = [startIndex, endIndex - 1];\n                }\n            }\n        }\n        return pill;\n    }\n}\n", "import { ganttView } from \"@web_gantt/gantt_view\";\nimport { HrGanttRenderer } from \"./hr_gantt_renderer\";\nimport { registry } from \"@web/core/registry\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const hrGanttView = {\n    ...ganttView,\n    Renderer: HrGanttRenderer,\n};\n\nviewRegistry.add(\"hr_gantt\", hrGanttView);\n", "/** @odoo-module */\n\nimport { HierarchyCard } from \"@web_hierarchy/hierarchy_card\";\n\nexport class HrEmployeeHierarchyCard extends HierarchyCard {\n    static template = \"hr_org_chart.HrEmployeeHierarchyCard\";\n}\n", "/** @odoo-module **/\n\nimport { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\n\nimport { HierarchyRenderer } from \"@web_hierarchy/hierarchy_renderer\";\nimport { HrEmployeeHierarchyCard } from \"./hr_employee_hierarchy_card\";\n\nexport class HrEmployeeHierarchyRenderer extends HierarchyRenderer {\n    static template = \"hr_org_chart.HrEmployeeHierarchyRenderer\";\n    static components = {\n        ...HierarchyRenderer.components,\n        HierarchyCard: HrEmployeeHierarchyCard,\n        Avatar,\n    };\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { hierarchyView } from \"@web_hierarchy/hierarchy_view\";\nimport { HrEmployeeHierarchyRenderer } from \"./hr_employee_hierarchy_renderer\";\nimport { HierarchyController } from \"@web_hierarchy/hierarchy_controller\";\nimport { HrActionHelper } from \"@hr/views/hr_action_helper\";\n\nexport class HrEmployeeHierarchyController extends HierarchyController {\n    static template = \"hr_org_chart.HierarchyView\";\n    static components = { ...HierarchyController.components, HrActionHelper };\n}\n\nexport const hrEmployeeHierarchyView = {\n    ...hierarchyView,\n    Controller: HrEmployeeHierarchyController,\n    Renderer: HrEmployeeHierarchyRenderer,\n};\n\nregistry.category(\"views\").add(\"hr_employee_hierarchy\", hrEmployeeHierarchyView);\n", "import { GraphRenderer } from \"@web/views/graph/graph_renderer\";\nimport { user } from \"@web/core/user\";\nimport { session } from \"@web/session\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SpreadsheetSelectorDialog } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\nimport { omit } from \"@web/core/utils/objects\";\n\nexport const patchGraphSpreadsheet = () => ({\n    setup() {\n        super.setup(...arguments);\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.menu = useService(\"menu\");\n        this.canInsertChart = session.can_insert_in_spreadsheet;\n    },\n\n    async onInsertInSpreadsheet() {\n        const { actionId } = this.env.config;\n        const { xml_id } = actionId\n            ? await this.actionService.loadAction(actionId, this.env.searchModel.context)\n            : {};\n        const actionOptions = {\n            preProcessingAsyncAction: \"insertChart\",\n            preProcessingAsyncActionData: {\n                metaData: this.model.metaData,\n                searchParams: {\n                    ...this.model.searchParams,\n                    domain: this.env.searchModel.domainString,\n                    context: omit(\n                        this.model.searchParams.context,\n                        ...Object.keys(user.context),\n                        \"graph_measure\",\n                        \"graph_order\"\n                    ),\n                },\n                actionXmlId: xml_id,\n            },\n        };\n        const params = {\n            type: \"GRAPH\",\n            name: this.model.metaData.title,\n            actionOptions,\n        };\n        this.env.services.dialog.add(SpreadsheetSelectorDialog, params);\n    },\n});\n\n/**\n * This patch is a little trick, which require a little explanation:\n *\n * In this patch, we add some dependencies to the graph view (menu service,\n * router service, ...).\n * To test it, we add these dependencies in our tests, but these dependencies\n * are not added in the tests of the base graph view (in web/). The same thing\n * occurs for the button \"Insert in spreadsheet\".\n * As we do not want to modify tests in web/ in order to integrate a behavior\n * defined in another module, we disable this patch in a file that is only\n * loaded in test assets (disable_patch.js), and re-active it in our tests.\n */\nexport const unpatchGraphSpreadsheet = patch(GraphRenderer.prototype, patchGraphSpreadsheet());\n", "import { PivotRenderer } from \"@web/views/pivot/pivot_renderer\";\nimport { user } from \"@web/core/user\";\nimport { intersection, unique } from \"@web/core/utils/arrays\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SpreadsheetSelectorDialog } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\n\nimport { session } from \"@web/session\";\n\n/**\n * This const is defined in o-spreadsheet library, but has to be redefined here\n * because o-spreadsheet is lazy loaded in another bundle than this file is.\n */\nconst ALL_PERIODS = {\n    quarter: _t(\"Quarter & Year\"),\n    month: _t(\"Month & Year\"),\n    week: _t(\"Week & Year\"),\n    day: _t(\"Day\"),\n    year: _t(\"Year\"),\n    quarter_number: _t(\"Quarter\"),\n    month_number: _t(\"Month\"),\n    iso_week_number: _t(\"Week\"),\n    day_of_month: _t(\"Day of Month\"),\n};\n\npatch(PivotRenderer.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.canInsertPivot = session.can_insert_in_spreadsheet;\n    },\n\n    async onInsertInSpreadsheet() {\n        let name = this.model.metaData.title;\n        const groupBy =\n            this.model.metaData.fullColGroupBys[0] || this.model.metaData.fullRowGroupBys[0];\n        if (groupBy) {\n            let [field, period] = groupBy.split(\":\");\n            period = ALL_PERIODS[period];\n            if (period) {\n                name = _t(\"%(pivot_title)s by %(group_by)s (%(granularity)s)\", {\n                    pivot_title: name,\n                    group_by: this.model.metaData.fields[field].string,\n                    granularity: period,\n                });\n            } else {\n                name = _t(\"%(pivot_title)s by %(group_by)s\", {\n                    pivot_title: name,\n                    group_by: this.model.metaData.fields[field].string,\n                });\n            }\n        }\n        const { actionId } = this.env.config;\n        const { xml_id } = actionId\n            ? await this.actionService.loadAction(actionId, this.env.searchModel.context)\n            : {};\n\n        const actionOptions = {\n            preProcessingAsyncAction: \"insertPivot\",\n            preProcessingAsyncActionData: {\n                metaData: this.model.metaData,\n                searchParams: {\n                    ...this.model.searchParams,\n                    domain: this.env.searchModel.domainString,\n                    context: omit(\n                        this.model.searchParams.context,\n                        ...Object.keys(user.context),\n                        \"pivot_measures\",\n                        \"pivot_row_groupby\",\n                        \"pivot_column_groupby\"\n                    ),\n                },\n                name,\n                actionXmlId: xml_id,\n            },\n        };\n        const params = {\n            type: \"PIVOT\",\n            name,\n            actionOptions,\n        };\n        this.env.services.dialog.add(SpreadsheetSelectorDialog, params);\n    },\n\n    hasDuplicatedGroupbys() {\n        const fullColGroupBys = this.model.metaData.fullColGroupBys;\n        const fullRowGroupBys = this.model.metaData.fullRowGroupBys;\n        // without aggregator\n        const colGroupBys = fullColGroupBys.map((el) => el.split(\":\")[0]);\n        const rowGroupBys = fullRowGroupBys.map((el) => el.split(\":\")[0]);\n        return (\n            unique([...fullColGroupBys, ...fullRowGroupBys]).length <\n                fullColGroupBys.length + fullRowGroupBys.length ||\n            // can group by the same field with different aggregator in the same dimension\n            intersection(colGroupBys, rowGroupBys).length\n        );\n    },\n\n    isInsertButtonDisabled() {\n        return (\n            !this.model.hasData() ||\n            this.model.metaData.activeMeasures.length === 0 ||\n            this.model.useSampleModel ||\n            this.hasDuplicatedGroupbys()\n        );\n    },\n\n    getInsertButtonTooltip() {\n        return this.hasDuplicatedGroupbys() ? _t(\"Pivot contains duplicate groupbys\") : undefined;\n    },\n});\n", "import { MapModel } from \"@web_map/map_view/map_model\";\n\nexport class StockMapModel extends MapModel {\n    _getRecordSpecification(metaData, data) {\n        return {\n            ...super._getRecordSpecification(metaData, data),\n            warehouse_address_id: {\n                fields: {\n                    display_name: {},\n                    contact_address_complete: {},\n                }\n            }\n        }\n    }\n}", "import { MapRenderer } from \"@web_map/map_view/map_renderer\";\n\nexport class StockMapRenderer extends MapRenderer {\n    get googleMapUrl() {\n        let url = super.googleMapUrl;\n        if (this.props.model.data.records.length) {\n            const warehouseAddress = this.props.model.data.records[0].warehouse_address_id;\n            let multiAddresses = false;\n            for (const record of this.props.model.data.records) {\n                if (record.warehouse_address_id.id !== warehouseAddress.id) {\n                    multiAddresses = true;\n                    break;\n                }\n            }\n            if (multiAddresses) {\n                return url;\n            }\n            url += `&origin=${warehouseAddress.contact_address_complete}`;\n            url += `&destination=${warehouseAddress.contact_address_complete}`;\n        }\n        return url;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { mapView } from \"@web_map/map_view/map_view\";\nimport { StockMapModel } from \"./map_model\";\nimport { StockMapRenderer } from \"./map_renderer\";\n\nexport const stockMapView = {\n    ...mapView,\n    Model: StockMapModel,\n    Renderer: StockMapRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_map\", stockMapView);", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nimport { Record } from \"@web/model/record\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { CharField } from \"@web/views/fields/char/char_field\";\nimport { TextField } from \"@web/views/fields/text/text_field\";\n\nimport { ThumbnailItem } from \"@web_studio/client_action/components/thumbnail_item/thumbnail_item\";\nimport { viewTypeToString, useStudioServiceAsReactive } from \"@web_studio/studio_service\";\nimport { NewViewDialog } from \"../editor/new_view_dialogs/new_view_dialog\";\nimport { MapNewViewDialog } from \"../editor/new_view_dialogs/map_new_view_dialog\";\n\nfunction getViewCategories() {\n    return {\n        general: {\n            title: _t(\"General views\"),\n            viewTypes: [\"form\", \"search\", \"activity\"],\n        },\n        multiple: {\n            title: _t(\"Multiple records views\"),\n            viewTypes: [\"list\", \"kanban\", \"map\"],\n        },\n        timeline: {\n            title: _t(\"Timeline views\"),\n            viewTypes: [\"calendar\", \"cohort\", \"gantt\"],\n        },\n        reporting: {\n            title: _t(\"Reporting views\"),\n            viewTypes: [\"graph\", \"pivot\"],\n        },\n    };\n}\n\nconst actionFieldsGet = {\n    id: { type: \"integer\" },\n    name: { type: \"char\" },\n    help: { type: \"text\" },\n    groups_id: { type: \"many2many\", relation: \"res.groups\", string: \"Groups\" },\n};\n\nfunction getActionActiveFields() {\n    const activeFields = {};\n    for (const fName of Object.keys(actionFieldsGet)) {\n        activeFields[fName] = {};\n    }\n\n    const groups_idRelated = Object.fromEntries(\n        many2ManyTagsField.relatedFields({ options: {} }).map((f) => [f.name, f])\n    );\n    activeFields.groups_id.related = { activeFields: groups_idRelated, fields: groups_idRelated };\n\n    return activeFields;\n}\n\nfunction getActionValues(action) {\n    const values = {};\n    for (const fName of Object.keys(actionFieldsGet)) {\n        values[fName] = action[fName];\n    }\n    return values;\n}\n\nclass ActionEditor extends Component {\n    static template = \"web_studio.ActionEditor\";\n    static props = { ...standardActionServiceProps };\n    static components = {\n        ThumbnailItem,\n        DropdownItem,\n        Record,\n        CharField,\n        TextField,\n        Many2ManyTagsField,\n    };\n\n    setup() {\n        this.studio = useStudioServiceAsReactive();\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.viewCategories = getViewCategories();\n        this.addDialog = useOwnedDialogs();\n\n        this.actionFieldsGet = { ...actionFieldsGet };\n    }\n\n    get actionRecordProps() {\n        const values = getActionValues(this.studio.editedAction);\n        return {\n            fields: this.actionFieldsGet,\n            resModel: \"ir.actions.act_window\",\n            resId: values.id,\n            mode: \"edit\",\n            values,\n            activeFields: getActionActiveFields(),\n            onRecordChanged: (record, changes) => {\n                return this.editAction(changes);\n            },\n        };\n    }\n\n    get activeViews() {\n        return this.studio.editedAction.views.map(([, name]) => name);\n    }\n\n    getOrderedViewTypes(viewTypes) {\n        const activeViews = this.activeViews;\n        const currentDefaultView = activeViews[0];\n        const viewInfos = viewTypes.map((viewType) => {\n            return {\n                name: viewType,\n                title: viewTypeToString(viewType),\n                isActive: activeViews.includes(viewType),\n                isDefault: currentDefaultView === viewType,\n                imgUrl: `/web_studio/static/src/img/view_type/${viewType}.png`,\n                canBeDefault: ![\"form\", \"search\"].includes(viewType),\n                canBeDisabled: viewType !== \"search\",\n            };\n        });\n        return sortBy(\n            viewInfos,\n            ({ isDefault, isActive }) => {\n                return isDefault ? 2 : isActive ? 1 : 0;\n            },\n            \"desc\"\n        );\n    }\n\n    setDefaultView(viewType) {\n        let viewModes = this.studio.editedAction.view_mode.split(\",\");\n        viewModes = viewModes.filter((m) => m !== viewType);\n        viewModes.unshift(viewType);\n        return this.editAction({ view_mode: viewModes.join(\",\") });\n    }\n\n    disableView(viewType) {\n        const viewMode = this.studio.editedAction.view_mode\n            .split(\",\")\n            .filter((m) => m !== viewType);\n\n        if (!viewMode.length) {\n            this.addDialog(AlertDialog, {\n                body: _t(\"You cannot deactivate this view as it is the last one active.\"),\n            });\n        } else {\n            return this.editAction({ view_mode: viewMode.join(\",\") });\n        }\n    }\n\n    restoreDefaultView(viewType) {\n        return this.env.editionFlow.restoreDefaultView(null, viewType);\n    }\n\n    async addViewType(viewType) {\n        const action = this.studio.editedAction;\n        const viewMode = action.view_mode.split(\",\");\n        viewMode.push(viewType);\n        let viewAdded = await rpc(\"/web_studio/add_view_type\", {\n            action_type: action.type,\n            action_id: action.id,\n            res_model: action.res_model,\n            view_type: viewType,\n            args: { view_mode: viewMode.join(\",\") },\n            context: user.context,\n        });\n\n        if (viewAdded !== true) {\n            viewAdded = await new Promise((resolve) => {\n                let DialogClass;\n                const dialogProps = {\n                    confirm: async () => {\n                        resolve(true);\n                    },\n                    cancel: () => resolve(false),\n                };\n                if ([\"gantt\", \"calendar\", \"cohort\"].includes(viewType)) {\n                    DialogClass = NewViewDialog;\n                    dialogProps.viewType = viewType;\n                } else if (viewType === \"map\") {\n                    DialogClass = MapNewViewDialog;\n                } else {\n                    this.addDialog(AlertDialog, {\n                        body: _t(\n                            \"Creating this type of view is not currently supported in Studio.\"\n                        ),\n                    });\n                    resolve(false);\n                }\n                this.addDialog(DialogClass, dialogProps);\n            });\n        }\n        if (viewAdded) {\n            await this.editAction({ view_mode: viewMode.join(\",\") });\n        }\n        return viewAdded;\n    }\n\n    editView(viewType) {\n        this.studio.setParams({ viewType, editorTab: \"views\" });\n    }\n\n    async onThumbnailClicked(viewType) {\n        if (this.activeViews.includes(viewType)) {\n            return this.editView(viewType);\n        }\n        const resModel = this.studio.editedAction.res_model;\n        if (viewType === \"activity\") {\n            const activityAllowed = await this.studio.isAllowed(\"activity\", resModel);\n            if (!activityAllowed) {\n                this.notification.add(\n                    _t(\"Activity view unavailable on this model\"),\n                    {\n                        title: false,\n                        type: \"danger\",\n                    }\n                );\n                return;\n            }\n        }\n        if (await this.addViewType(viewType)) {\n            return this.editView(viewType);\n        }\n    }\n\n    openFormAction() {\n        return this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"ir.actions.act_window\",\n                res_id: this.studio.editedAction.id,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            },\n            {\n                stackPosition: \"replacePreviousAction\",\n            }\n        );\n    }\n\n    async editAction(changes) {\n        await rpc(\"/web_studio/edit_action\", {\n            action_id: this.studio.editedAction.id,\n            action_type: \"ir.actions.act_window\",\n            args: changes,\n        });\n        return this.studio.reload({}, false);\n    }\n}\n\nregistry.category(\"actions\").add(\"web_studio.action_editor\", ActionEditor);\n", "/** @odoo-module **/\n\nimport { Component, reactive, useExternalListener, useState } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { ModelConfigurator } from \"@web_studio/client_action/model_configurator/model_configurator\";\nimport { DEFAULT_ICON, IconCreator } from \"../icon_creator/icon_creator\";\nimport { MenuCreator, MenuCreatorModel } from \"@web_studio/client_action/menu_creator/menu_creator\";\n\nclass AppCreatorState {\n    /**\n     * @param {Function} onFinished\n     */\n    constructor({ onFinished }) {\n        this._onFinished = onFinished;\n        // ==================== Misc ====================\n        this.step = \"welcome\";\n\n        // ================== Fields ==================\n        this.fieldsValidators = {\n            appName: () => !!this.data.appName,\n            menu: (_this) => _this.menuCreatorModel.isValid,\n        };\n        this.menuCreatorModel = reactive(new MenuCreatorModel());\n\n        this.data = {\n            appName: \"\",\n            iconData: DEFAULT_ICON,\n            menu: this.menuCreatorModel.data,\n            modelOptions: [],\n        };\n\n        // ================== Steps ==================\n        this._steps = {\n            welcome: {\n                next: () => \"app\",\n            },\n            app: {\n                previous: \"welcome\",\n                next: () => \"model\",\n                fields: [\"appName\"],\n            },\n            model: {\n                previous: \"app\",\n                next: (data) => {\n                    return data.menu.modelChoice === \"new\" ? \"model_configuration\" : \"\";\n                },\n                fields: [\"menu\"],\n            },\n            model_configuration: {\n                previous: \"model\",\n            },\n        };\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    get step() {\n        return this._step;\n    }\n\n    set step(step) {\n        this._step = step;\n        this.showValidation = false;\n    }\n\n    get nextStep() {\n        return this._stepInvalidFields.length ? false : this._next;\n    }\n\n    get hasPrevious() {\n        return \"previous\" in this._currentStep;\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    isFieldValid(fieldName) {\n        return this.showValidation ? this.fieldsValidators[fieldName](this) : true;\n    }\n\n    next() {\n        this.showValidation = true;\n        const invalidFields = this._stepInvalidFields;\n        if (invalidFields.length) {\n            return;\n        }\n        const next = this._next;\n        if (next) {\n            this.step = next;\n        } else {\n            return this._onFinished();\n        }\n    }\n\n    previous() {\n        if (this._currentStep.previous) {\n            this.step = this._currentStep.previous;\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    get _currentStep() {\n        return this._steps[this._step];\n    }\n\n    get _next() {\n        return this._currentStep.next ? this._currentStep.next(this.data) : \"\";\n    }\n\n    get _stepInvalidFields() {\n        return (this._currentStep.fields || []).filter((fName) => {\n            return !this.fieldsValidators[fName](this);\n        });\n    }\n}\n\nexport class AppCreator extends Component {\n    static template = \"web_studio.AppCreator\";\n    static components = { IconCreator, ModelConfigurator, MenuCreator };\n    static props = {\n        onNewAppCreated: { type: Function },\n    };\n\n    setup() {\n        this.state = useState(\n            new AppCreatorState({\n                onFinished: this.createNewApp.bind(this),\n            })\n        );\n\n        this.uiService = useService(\"ui\");\n\n        useAutofocus();\n        useExternalListener(window, \"keydown\", this.onKeydown);\n    }\n\n    /**\n     * @returns {Promise}\n     */\n    async createNewApp() {\n        this.uiService.block();\n        const data = this.state.data;\n        const iconData = data.iconData;\n\n        const iconValue =\n            iconData.type === \"custom_icon\"\n                ? // custom icon data\n                  [iconData.iconClass, iconData.color, iconData.backgroundColor]\n                : // attachment\n                  iconData.uploaded_attachment_id;\n\n        try {\n            const result = await rpc(\"/web_studio/create_new_app\", {\n                app_name: data.appName,\n                menu_name: data.menu.menuName,\n                model_choice: data.menu.modelChoice,\n                model_id: data.menu.modelChoice && data.menu.modelId && data.menu.modelId[0],\n                model_options: data.modelOptions,\n                icon: iconValue,\n                context: user.context,\n            });\n            await this.props.onNewAppCreated(result);\n        } finally {\n            this.uiService.unblock();\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onKeydown(ev) {\n        if (\n            ev.key === \"Enter\" &&\n            !(\n                ev.target.classList &&\n                ev.target.classList.contains(\"o_web_studio_app_creator_previous\")\n            )\n        ) {\n            ev.preventDefault();\n            this.state.next();\n        }\n    }\n\n    /**\n     * Handle the confirmation of options in the modelconfigurator\n     * @param {Object} options\n     */\n    onConfirmOptions(options) {\n        this.state.data.modelOptions = options;\n        return this.state.next();\n    }\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { getFontAwesomeIcons } from \"@web_studio/utils\";\n\nexport class FontAwesomeIconSelector extends Component {\n    static defaultProps = {\n        className: \"\",\n        menuClassName: \"\",\n    };\n    static template = \"web_studio.FontAwesomeIconSelector\";\n    static props = {\n        className: { type: String, optional: true },\n        menuClassName: { type: String, optional: true },\n        value: { type: String },\n        onSelect: { type: Function, optional: true },\n        slots: true,\n    };\n    static components = { SelectMenu };\n\n    setup() {\n        this.ICONS = getFontAwesomeIcons();\n    }\n\n    get iconChoices() {\n        return this.ICONS.map((icon) => {\n            return {\n                label: icon.searchTerms.join(\" \"),\n                value: icon.className,\n            };\n        });\n    }\n\n    getIconTooltip(value) {\n        return this.ICONS.find((icon) => icon.className === value).tooltip;\n    }\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\n\nexport class SidebarDraggableItem extends Component {\n    static template = \"web_studio.SidebarDraggableItem\";\n    static props = {\n        className: { type: String, optional: true },\n        description: { type: String, optional: true },\n        dropData: { optional: true },\n        string: { type: String },\n        structure: { type: String },\n    };\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n\nexport class ThumbnailItem extends Component {\n    static defaultProps = {\n        showMoreMenu: true,\n        onClick: () => {},\n    };\n    static template = \"web_studio.ThumbnailItem\";\n    static props = {\n        className: { type: String, optional: true },\n        showMoreMenu: { type: Boolean, optional: true },\n        icon: { type: Object },\n        onClick: { type: Function, optional: true },\n        slots: true,\n    };\n    static components = { Dropdown };\n\n    get hasDropdown() {\n        return this.props.showMoreMenu && this.props.slots && this.props.slots.dropdown;\n    }\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useBus, useService, useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { user } from \"@web/core/user\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { MenuCreatorDialog } from \"@web_studio/client_action/menu_creator/menu_creator\";\nimport { useDialogConfirmation, useSubEnvAndServices } from \"@web_studio/client_action/utils\";\n\nconst EditMenuDialogProps = { ...Dialog.props };\nEditMenuDialogProps.close = { type: Function };\ndelete EditMenuDialogProps.slots;\nclass EditMenuDialog extends Component {\n    static components = { Dialog };\n    static template = \"web_studio.AppMenuEditor.EditMenuDialog\";\n    static props = EditMenuDialogProps;\n\n    setup() {\n        // Keep the bus from the WebClient\n        const originalBus = this.env.bus;\n        useBus(originalBus, \"MENUS:APP-CHANGED\", () => (this.state.tree = this.getTree()));\n\n        this.menus = useService(\"menu\");\n        this.addDialog = useOwnedDialogs();\n        this.orm = useService(\"orm\");\n\n        useBus(this.env.bus, \"ACTION_MANAGER:UPDATE\", () => this.cancel());\n\n        // States and data\n        this.state = useState({ tree: {}, flatMenus: {} });\n        this.state.tree = this.getTree();\n        this.toMove = {};\n        this.toDelete = [];\n\n        // DragAndDrop to move menus around\n        const root = useRef(\"root\");\n        useNestedSortable({\n            ref: root,\n            handle: \".o-draggable-handle\",\n            nest: true,\n            maxLevels: 5,\n            useElementSize: true,\n            onDrop: this.moveMenu.bind(this),\n        });\n\n        const { confirm, cancel } = useDialogConfirmation({\n            confirm: async () => {\n                await this.saveChanges();\n            },\n        });\n        this.confirm = confirm;\n        this.cancel = cancel;\n    }\n\n    get flatMenus() {\n        return this.state.flatMenus;\n    }\n\n    get mainItem() {\n        return this.state.tree;\n    }\n\n    getTree() {\n        let currentApp = this.menus.getCurrentApp();\n        if (!currentApp) {\n            return null;\n        }\n        currentApp = this.menus.getMenuAsTree(currentApp.id);\n        const item = this._getItemFromMenu(currentApp, null);\n        item.isDraggable = false;\n        item.isRemovable = false;\n        return item;\n    }\n\n    _getItemFromMenu(menu, parentId) {\n        const item = {\n            id: menu.id,\n            name: menu.name,\n            isDraggable: true,\n            isRemovable: true,\n            parentId,\n        };\n        item.children = menu.childrenTree.map((menu) => this._getItemFromMenu(menu, item.id));\n        this.flatMenus[item.id] = item;\n        return item;\n    }\n\n    moveMenu({ element, parent, previous }) {\n        const menuId = parseInt(element.dataset.itemId);\n        const menu = this.flatMenus[menuId];\n\n        // Remove element from parent's children (since we are moving it, this is the mandatory first step)\n        let parentMenu = this.flatMenus[menu.parentId];\n        parentMenu.children = parentMenu.children.filter((m) => m.id !== menuId);\n\n        // Determine next parent\n        const parentLi = parent?.closest(\"li\");\n        const parentMenuId = parentLi ? parseInt(parentLi.dataset.itemId) : this.mainItem.id;\n        if (parentMenuId !== parentMenu.id) {\n            parentMenu = this.flatMenus[parentMenuId];\n            menu.parentId = parentMenu.id;\n        }\n\n        // Determine at which position we should place the element\n        if (previous) {\n            const previousMenu = this.flatMenus[previous.dataset.itemId];\n            const index = parentMenu.children.findIndex((child) => child === previousMenu);\n            parentMenu.children.splice(index + 1, 0, menu);\n        } else {\n            parentMenu.children.unshift(menu);\n        }\n\n        // Last step: prepare the data that can be sent to the server.\n        this.toMove[menuId] = {\n            parent_menu_id: menu.parentId,\n        };\n\n        parentMenu.children.forEach((m, index) => {\n            this.toMove[m.id] = this.toMove[m.id] || {};\n            this.toMove[m.id].sequence = index + 1;\n        });\n    }\n\n    removeItem(menu) {\n        const parentMenu = this.flatMenus[menu.parentId];\n        if (!parentMenu) {\n            return;\n        }\n        parentMenu.children = parentMenu.children.filter((m) => m.id !== menu.id);\n        this.toDelete.push(menu.id);\n    }\n\n    editItem(menu) {\n        this.addDialog(FormViewDialog, {\n            resModel: \"ir.ui.menu\",\n            resId: menu.id,\n            onRecordSaved: async () => {\n                await this.saveChanges(true);\n            },\n        });\n    }\n\n    async saveChanges(reload = false) {\n        if (Object.keys(this.toMove).length || this.toDelete.length) {\n            await this.orm.call(\"ir.ui.menu\", \"customize\", [], {\n                to_move: this.toMove,\n                to_delete: this.toDelete,\n            });\n            reload = true;\n        }\n        if (reload) {\n            await this.menus.reload();\n        }\n    }\n\n    onNewMenu() {\n        this.addDialog(MenuCreatorDialog, {\n            confirm: async (data) => {\n                await rpc(\"/web_studio/create_new_menu\", {\n                    menu_name: data.menuName,\n                    model_id: data.modelId[0],\n                    model_choice: data.modelChoice,\n                    model_options: data.modelOptions || {},\n                    parent_menu_id: this.mainItem.id,\n                    context: user.context,\n                });\n                this.env.bus.trigger(\"CLEAR-CACHES\");\n                this.menus.reload();\n            },\n        });\n    }\n}\n\nexport class AppMenuEditor extends Component {\n    static props = {\n        env: { type: Object },\n    };\n    static template = \"web_studio.AppMenuEditor\";\n\n    setup() {\n        this.menus = useService(\"menu\");\n        // original bus from webClient\n        const bus = this.env.bus;\n        // ovverride the whole env coming from within studio\n        // contains an override of dialog and an override of action\n        useSubEnvAndServices(this.props.env);\n        this.addDialog = useOwnedDialogs();\n        useBus(bus, \"MENUS:APP-CHANGED\", () => this.render());\n    }\n\n    onClick(ev) {\n        ev.preventDefault();\n        this.addDialog(EditMenuDialog);\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { toRaw, useState, useEnv, reactive, onMounted, onWillUnmount, markRaw } from \"@odoo/owl\";\nimport { Reactive } from \"@web_studio/client_action/utils\";\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\n/**\n * Provides standard shortcuts for the ActionEditor and  ViewEditor.\n * Used as a communcation interface between the editorMenu\n * and the ActionEditor or the ViewEditor\n *\n * Supports snackBar, breadcrumbs, operation stack\n */\nexport class EditionFlow extends Reactive {\n    constructor(env, services) {\n        super();\n        this.env = env;\n        for (const [servName, serv] of Object.entries(services)) {\n            this[servName] = serv;\n        }\n        this.setup();\n    }\n    setup() {\n        let requestId;\n        const updateBreadcrumbs = (studio) => {\n            if (studio.requestId !== requestId) {\n                this.breadcrumbs = [];\n            }\n            requestId = studio.requestId;\n        };\n        const studio = reactive(this.studio, () => updateBreadcrumbs(studio));\n        this.studio = studio;\n        this.breadcrumbs = [];\n        updateBreadcrumbs(studio);\n    }\n\n    pushBreadcrumb(crumb) {\n        const bcLength = this.breadcrumbs.length;\n        const handler = () => {\n            this.breadcrumbs.length = bcLength + 1;\n            // Reset studio to its own state\n            // In case another action has been done\n            this.studio.setParams({}, false);\n        };\n        this.breadcrumbs.push({ data: crumb, handler });\n    }\n\n    async loadViews({ forceSearch = false } = {}) {\n        const editedAction = this.studio.editedAction;\n        const { context, res_model, id } = editedAction;\n        const views = [...editedAction.views];\n        if (forceSearch && !views.some((tuple) => tuple[1] === \"search\")) {\n            views.push([false, \"search\"]);\n        }\n        const newContext = { ...context, lang: false };\n        const options = { loadIrFilters: true, loadActionMenus: false, id, studio: true };\n        const res = await this.view.loadViews(\n            { resModel: res_model, views, context: newContext },\n            options\n        );\n        return JSON.parse(JSON.stringify(res));\n    }\n    restoreDefaultView(viewId, viewType) {\n        return new Promise((resolve) => {\n            const confirm = async () => {\n                if (!viewId && viewType) {\n                    // To restore the default view from an inherited one, we need first to retrieve the default view id\n                    const result = await this.loadViews();\n                    viewId = result.views[viewType].id;\n                }\n                const res = await rpc(\"/web_studio/restore_default_view\", {\n                    view_id: viewId,\n                });\n                this.env.bus.trigger(\"CLEAR-CACHES\");\n                resolve(res);\n            };\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\n                    \"Are you sure you want to restore the default view?\\r\\nAll customization done with studio on this view will be lost.\"\n                ),\n                confirm,\n                cancel: () => resolve(false),\n            });\n        });\n    }\n}\n\nexport function useEditorBreadcrumbs(initialCrumb) {\n    const env = useEnv();\n    const editionFlow = env.editionFlow;\n\n    if (initialCrumb && !editionFlow.breadcrumbs.length) {\n        onMounted(() => editionFlow.pushBreadcrumb(initialCrumb));\n    }\n\n    const crumbs = useState(editionFlow.breadcrumbs);\n    const push = (crumb) => editionFlow.pushBreadcrumb(crumb);\n    return { crumbs, push };\n}\n\nexport function useEditorMenuItem(MenuItem) {\n    const editionFlow = useEnv().editionFlow;\n    onMounted(() => {\n        editionFlow.MenuItem = MenuItem;\n    });\n    onWillUnmount(() => {\n        if (toRaw(editionFlow).MenuItem === MenuItem) {\n            editionFlow.MenuItem = null;\n        }\n    });\n}\n\n/**\n * Indicates whether a the concrete editor has finished its async operation\n * with its state: loaded/loading\n */\n\n// PAss state instead of proms\n// PAss count ? => error handling ?\n// ecrase prom precedente ?\n//\nexport class SnackbarIndicator extends Reactive {\n    constructor() {\n        super();\n        this.state = \"\";\n        this.keepLast = markRaw(new KeepLast());\n    }\n\n    add(prom) {\n        this.state = \"loading\";\n        const raw = this.raw();\n        this.pending = Promise.all([raw.pending, prom]);\n\n        this.keepLast\n            .add(raw.pending)\n            .then(() => (this.state = \"loaded\"))\n            .catch(() => {\n                this.state = \"error\";\n            })\n            .finally(() => {\n                this.pending = null;\n            });\n        return prom;\n    }\n}\n\n/**\n * A Class that manages undo/redo of some operations\n * in a sort of MutexedKeeLast: doing many calls to \"do\"\n * will just store the arguments and keep only the last call's results\n */\nexport class EditorOperations extends Reactive {\n    constructor(params) {\n        super();\n        this.operations = [];\n        this.undone = [];\n        this._lock = \"\";\n        this._keepLast = markRaw(new KeepLast());\n\n        this._callbacks = {\n            do: params.do,\n            onError: params.onError,\n            onDone: params.onDone,\n        };\n    }\n\n    get canUndo() {\n        return this.operations.length || (this.pending && this.pending.length);\n    }\n\n    get canRedo() {\n        return this.undone.length || (this.pendingUndone && this.pendingUndone.length);\n    }\n\n    _wrapPromise(prom) {\n        return this._keepLast.add(prom);\n    }\n\n    _prepare(mode) {\n        const raw = this.raw();\n        const lock = raw._lock;\n        if (lock && lock !== mode) {\n            this._wrapPromise(Promise.resolve());\n            this._close();\n            return false;\n        }\n        this._lock = mode;\n        const pending = raw.pending;\n        if (!pending) {\n            this.pending = [...raw.operations];\n            this.pendingUndone = [...raw.undone];\n        }\n        return true;\n    }\n\n    async _do(mode, pending, lastOp) {\n        let result;\n        let error;\n        try {\n            result = await this._wrapPromise(\n                this._callbacks.do({ mode, operations: pending, lastOp })\n            );\n        } catch (e) {\n            error = e;\n        }\n        if (error) {\n            return { error };\n        }\n        return { result };\n    }\n\n    _close(done = null) {\n        const raw = this.raw();\n        const mode = raw._lock;\n        this._lock = null;\n        const pending = raw.pending;\n        const pendingUndone = raw.pendingUndone;\n        this.pending = null;\n        this.pendingUndone = null;\n\n        if (!done) {\n            return;\n        }\n\n        if (\"result\" in done) {\n            this.operations = pending;\n            this.undone = pendingUndone;\n            if (typeof done.result !== \"boolean\") {\n                return this._callbacks.onDone({\n                    mode,\n                    pending,\n                    pendingUndone,\n                    result: done.result,\n                });\n            }\n        }\n        if (\"error\" in done) {\n            return this._callbacks.onError({ mode, pending, error: done.error });\n        }\n    }\n\n    async undo(canRedo = true) {\n        if (!this._prepare(\"undo\")) {\n            this._close();\n            return;\n        }\n        const ops = this.raw().pending;\n        if (!ops || !ops.length) {\n            this._close();\n            return;\n        }\n        const op = ops.pop();\n        if (canRedo) {\n            this.pendingUndone.push(op);\n        }\n        const done = await this._do(\"undo\", this.raw().pending, op);\n        this._close(done);\n    }\n\n    pushOp(op) {\n        this.operations.push(op);\n    }\n\n    async redo() {\n        if (!this._prepare(\"redo\")) {\n            this._close();\n            return;\n        }\n\n        const ops = this.raw().pendingUndone;\n        if (!ops || !ops.length) {\n            this._close();\n            return;\n        }\n        const op = ops.pop();\n        this.pending.push(op);\n        const done = await this._do(\"do\", this.raw().pending, op);\n        this._close(done);\n    }\n\n    async doMulti(ops = []) {\n        if (!ops.length) {\n            return;\n        }\n        let prom;\n        for (let i = 0; i < ops.length; i++) {\n            let silent = true;\n            if (i === ops.length - 1) {\n                silent = false;\n            }\n            prom = this.do(ops[i], silent);\n        }\n        return prom;\n    }\n\n    async do(op, silent = false) {\n        if (!this._prepare(\"do\") || !op) {\n            this._close();\n            return;\n        }\n        this.pending.push(op);\n        this.pendingUndone = [];\n        let done = {};\n        if (!silent) {\n            done = await this._do(\"do\", this.raw().pending, op);\n        } else {\n            done = { result: true };\n        }\n        this._close(done);\n    }\n\n    clear(all = true) {\n        this.operations = [];\n        if (all) {\n            this.undone = [];\n        }\n    }\n}\n", "/** @odoo-module **/\nimport { Component, EventBus, onWillDestroy, useState, useSubEnv, xml } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { makeActionManager } from \"@web/webclient/actions/action_service\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nimport { StudioActionContainer } from \"./studio_action_container\";\nimport { EditorMenu } from \"./editor_menu/editor_menu\";\n\nimport { AppMenuEditor } from \"./app_menu_editor/app_menu_editor\";\nimport { NewModelItem } from \"./new_model_item/new_model_item\";\nimport { EditionFlow } from \"./edition_flow\";\nimport { useStudioServiceAsReactive } from \"@web_studio/studio_service\";\nimport { useSubEnvAndServices, useServicesOverrides } from \"@web_studio/client_action/utils\";\nimport { omit } from \"@web/core/utils/objects\";\n\nclass DialogWithEnv extends Component {\n    static template = xml`<t t-component=\"props.Component\" t-props=\"componentProps\" />`;\n    static props = [\"*\"];\n\n    setup() {\n        useSubEnvAndServices(this.props.env);\n    }\n\n    get componentProps() {\n        const additionalProps = omit(this.props, \"Component\", \"env\", \"componentProps\");\n        return { ...this.props.componentProps, ...additionalProps };\n    }\n}\nconst dialogService = {\n    dependencies: [\"dialog\"],\n    start(env, { dialog }) {\n        function addDialog(Component, _props, options) {\n            const props = { env, Component, componentProps: _props };\n            return dialog.add(DialogWithEnv, props, options);\n        }\n        return { ...dialog, add: addDialog };\n    },\n};\n\nconst actionServiceStudio = {\n    dependencies: [\"studio\", \"dialog\"],\n    start(env, { studio }) {\n        const router = {\n            current: { hash: {} },\n            pushState() {},\n            hideKeyFromUrl: () => {},\n        };\n        const action = makeActionManager(env, router);\n        const _doAction = action.doAction;\n\n        async function doAction(actionRequest, options) {\n            if (actionRequest === \"web_studio.action_edit_report\") {\n                return studio.setParams({\n                    editedReport: options.report,\n                });\n            }\n            return _doAction(...arguments);\n        }\n\n        return Object.assign(action, { doAction });\n    },\n};\n\nconst menuButtonsRegistry = registry.category(\"studio_navbar_menubuttons\");\nexport class Editor extends Component {\n    static template = \"web_studio.Editor\";\n    static props = {};\n    static components = {\n        EditorMenu,\n        StudioActionContainer,\n    };\n\n    static menuButtonsId = 1;\n    setup() {\n        const globalBus = this.env.bus;\n        const newBus = new EventBus();\n        useBus(globalBus, \"CLEAR-UNCOMMITTED-CHANGES\", (ev) =>\n            newBus.trigger(\"CLEAR-UNCOMMITTED-CHANGES\", ev.detail)\n        );\n        useBus(globalBus, \"MENUS:APP-CHANGED\", (ev) =>\n            newBus.trigger(\"MENUS:APP-CHANGED\", ev.detail)\n        );\n        newBus.addEventListener(\"CLEAR-CACHES\", () => globalBus.trigger(\"CLEAR-CACHES\"));\n\n        useSubEnv({\n            bus: newBus,\n        });\n\n        useServicesOverrides({\n            dialog: dialogService,\n            action: actionServiceStudio,\n        });\n        this.studio = useService(\"studio\");\n\n        const editionFlow = new EditionFlow(this.env, {\n            dialog: useService(\"dialog\"),\n            studio: useStudioServiceAsReactive(),\n            view: useService(\"view\"),\n        });\n        useSubEnv({\n            editionFlow,\n        });\n\n        this.actionService = useService(\"action\");\n\n        this.state = useState({ actionContainerId: 1 });\n        useBus(this.studio.bus, \"UPDATE\", async () => {\n            this.state.actionContainerId++;\n        });\n\n        // Push instance-specific components in the navbar. Because we want those elements\n        // immediately, we add them at setup time, not onMounted.\n        // Also, because they are Editor instance-specific, and that Destroyed is mostly called\n        // after the new instance is created, we need to remove the old entries before adding the new ones\n        menuButtonsRegistry.getEntries().forEach(([name]) => {\n            if (name.startsWith(\"app_menu_editor_\") || name.startsWith(\"new_model_item_\")) {\n                menuButtonsRegistry.remove(name);\n            }\n        });\n        const menuButtonsId = this.constructor.menuButtonsId++;\n        menuButtonsRegistry.add(`app_menu_editor_${menuButtonsId}`, {\n            Component: AppMenuEditor,\n            props: { env: this.env },\n        });\n        menuButtonsRegistry.add(`new_model_item_${menuButtonsId}`, { Component: NewModelItem });\n        onWillDestroy(() => {\n            menuButtonsRegistry.remove(`app_menu_editor_${menuButtonsId}`);\n            menuButtonsRegistry.remove(`new_model_item_${menuButtonsId}`);\n        });\n    }\n\n    switchView({ viewType }) {\n        this.studio.setParams({ viewType, editorTab: \"views\" });\n    }\n    switchViewLegacy(ev) {\n        this.studio.setParams({ viewType: ev.detail.view_type });\n    }\n\n    switchTab({ tab }) {\n        this.studio.setParams({ editorTab: tab });\n    }\n}\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { useStudioServiceAsReactive } from \"@web_studio/studio_service\";\nconst editorTabRegistry = registry.category(\"web_studio.editor_tabs\");\n\nclass Breadcrumbs extends Component {\n    static template = \"web_studio.EditorMenu.Breadcrumbs\";\n    static props = {\n        currentTab: { type: Object },\n        switchTab: Function,\n    };\n    setup() {\n        this.editionFlow = useState(this.env.editionFlow);\n        this.nextCrumbId = 1;\n    }\n    get breadcrumbs() {\n        const currentTab = this.props.currentTab;\n        const crumbs = [\n            {\n                data: {\n                    name: currentTab.name,\n                },\n                handler: () => this.props.switchTab({ tab: currentTab.id }),\n            },\n        ];\n        const breadcrumbs = this.editionFlow.breadcrumbs;\n        breadcrumbs.forEach((crumb) => {\n            crumbs.push(crumb);\n        });\n        for (const crumb of crumbs) {\n            crumb.id = this.nextCrumbId++;\n        }\n        return crumbs;\n    }\n}\n\nexport class EditorMenu extends Component {\n    static props = {\n        switchTab: Function,\n        switchView: Function,\n    };\n    static template = \"web_studio.EditorMenu\";\n    static viewTypes = [\n        {\n            title: _t(\"Form\"),\n            type: \"form\",\n            iconClasses: \"fa fa-address-card\",\n        },\n        {\n            title: _t(\"List\"),\n            type: \"list\",\n            iconClasses: \"oi oi-view-list\",\n        },\n        {\n            title: _t(\"Kanban\"),\n            type: \"kanban\",\n            iconClasses: \"oi oi-view-kanban\",\n        },\n        {\n            title: _t(\"Map\"),\n            type: \"map\",\n            iconClasses: \"fa fa-map-marker\",\n        },\n        {\n            title: _t(\"Calendar\"),\n            type: \"calendar\",\n            iconClasses: \"fa fa-calendar\",\n        },\n        {\n            title: _t(\"Graph\"),\n            type: \"graph\",\n            iconClasses: \"fa fa-area-chart\",\n        },\n        {\n            title: _t(\"Pivot\"),\n            type: \"pivot\",\n            iconClasses: \"oi oi-view-pivot\",\n        },\n        {\n            title: _t(\"Gantt\"),\n            type: \"gantt\",\n            iconClasses: \"fa fa-tasks\",\n        },\n        {\n            title: _t(\"Cohort\"),\n            type: \"cohort\",\n            iconClasses: \"oi oi-view-cohort\",\n        },\n        {\n            title: _t(\"Activity\"),\n            type: \"activity\",\n            iconClasses: \"fa fa-clock-o\",\n        },\n        {\n            title: _t(\"Search\"),\n            type: \"search\",\n            iconClasses: \"oi oi-search\",\n        },\n    ];\n\n    static components = { Breadcrumbs };\n    setup() {\n        this.l10n = localization;\n        this.studio = useStudioServiceAsReactive();\n        this.editionFlow = useState(this.env.editionFlow);\n    }\n\n    get activeViews() {\n        const action = this.studio.editedAction;\n        const viewTypes = (action._views || action.views).map(([, type]) => type);\n        return this.constructor.viewTypes.filter((vt) => viewTypes.includes(vt.type));\n    }\n\n    get editorTabs() {\n        const entries = editorTabRegistry.getEntries();\n        return entries.map((entry) => Object.assign({}, entry[1], { id: entry[0] }));\n    }\n\n    get currentTab() {\n        return this.editorTabs.find((tab) => tab.id === this.studio.editorTab);\n    }\n\n    openTab(tab) {\n        this.props.switchTab({ tab });\n    }\n}\n\neditorTabRegistry\n    .add(\"views\", { name: _t(\"Views\"), action: \"web_studio.action_editor\" })\n    .add(\"reports\", { name: _t(\"Reports\") })\n    .add(\"automations\", { name: _t(\"Automations\") })\n    .add(\"automation_webhooks\", { name: _t(\"Webhooks\") })\n    .add(\"acl\", { name: _t(\"Access Control\") })\n    .add(\"filters\", { name: _t(\"Filter Rules\") });\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useService, useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { ModelConfiguratorDialog } from \"../../model_configurator/model_configurator\";\nimport { useDialogConfirmation } from \"../../utils\";\n\nclass SimpleNewModelDialog extends Component {\n    static template = \"web_studio.SimpleNewModelDialog\";\n    static components = { Dialog };\n    static props = { close: { type: Function } };\n\n    setup() {\n        this.addDialog = useOwnedDialogs();\n        this.menus = useService(\"menu\");\n        this.action = useService(\"action\");\n        this.studio = useService(\"studio\");\n        this.state = useState({ modelName: \"\", showValidation: false });\n        const { confirm, cancel } = useDialogConfirmation({\n            confirm: async (data) => {\n                const { menu_id, action_id } = await rpc(\"/web_studio/create_new_menu\", {\n                    menu_name: this.state.modelName,\n                    model_id: false,\n                    model_choice: \"new\",\n                    model_options: data.modelOptions,\n                    parent_menu_id: this.menus.getCurrentApp().id,\n                    context: user.context,\n                });\n                await this.menus.reload();\n                const action = await this.action.loadAction(action_id);\n                this.menus.setCurrentMenu(menu_id);\n                this.studio.setParams({ action, viewType: \"form\" });\n            },\n        });\n\n        this._confirm = confirm;\n        this._cancel = cancel;\n    }\n\n    confirm(data = {}) {\n        return this._confirm(data);\n    }\n\n    onConfigureModel() {\n        if (!this.state.modelName) {\n            this.state.showValidation = true;\n            return;\n        }\n\n        this.addDialog(ModelConfiguratorDialog, {\n            confirmLabel: _t(\"Create Model\"),\n            confirm: (data) => {\n                this.confirm({ modelOptions: data });\n            },\n        });\n    }\n}\n\nexport class NewModelItem extends Component {\n    static props = {};\n    static template = \"web_studio.NewModelItem\";\n\n    setup() {\n        this.addDialog = useOwnedDialogs();\n        this.menus = useService(\"menu\");\n        this.studio = useService(\"studio\");\n        this.action = useService(\"action\");\n\n        useBus(this.env.bus, \"MENUS:APP-CHANGED\", () => this.render());\n    }\n\n    onClick(ev) {\n        ev.preventDefault();\n        this.addDialog(SimpleNewModelDialog);\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { NewViewDialog } from \"@web_studio/client_action/editor/new_view_dialogs/new_view_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class MapNewViewDialog extends NewViewDialog {\n    static template = \"web_studio.MapNewViewDialog\";\n    static props = {\n        ...NewViewDialog.props,\n    };\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.fieldsChoice = {\n            res_partner: null,\n        };\n    }\n\n    get viewType() {\n        return \"map\";\n    }\n\n    computeSpecificFields(fields) {\n        this.partnerFields = fields.filter(\n            (field) => field.type === \"many2one\" && field.relation === \"res.partner\"\n        );\n        if (!this.partnerFields.length) {\n            this.dialog.add(AlertDialog, {\n                title: _t(\"Contact Field Required\"),\n                body: _t(\"Map views are based on the address of a linked Contact. You need to have a Many2one field linked to the res.partner model in order to create a map view.\"),\n                contentClass: \"o_web_studio_preserve_space\",\n            });\n            this.props.close();\n        } else {\n            this.fieldsChoice.res_partner = this.partnerFields[0].name;\n        }\n    }\n}\n\ndelete MapNewViewDialog.props.viewType;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class NewViewDialog extends ConfirmationDialog {\n    static template = \"web_studio.NewViewDialog\";\n    static GROUPABLE_TYPES = [\"many2one\", \"char\", \"boolean\", \"selection\", \"date\", \"datetime\"];\n    static MEASURABLE_TYPES = [\"integer\", \"float\"];\n    static props = {\n        ...ConfirmationDialog.props,\n        viewType: String,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.studio = useService(\"studio\");\n        this.mandatoryStopDate = [\"gantt\", \"cohort\"].includes(this.viewType);\n\n        this.title = _t(\"Generate %s View\", this.viewType);\n\n        this.fieldsChoice = {\n            date_start: null,\n            date_stop: null,\n        };\n\n        onWillStart(async () => {\n            const fieldsGet = await this.orm.call(this.studio.editedAction.res_model, \"fields_get\");\n            const fields = Object.entries(fieldsGet).map(([fName, field]) => {\n                field.name = fName;\n                return field;\n            });\n            fields.sort((first, second) => {\n                if (first.string === second.string) {\n                    return 0;\n                }\n                if (first.string < second.string) {\n                    return -1;\n                }\n                if (first.string > second.string) {\n                    return 1;\n                }\n            });\n            this.computeSpecificFields(fields);\n        });\n    }\n\n    get viewType() {\n        return this.props.viewType;\n    }\n\n    /**\n     * Compute date, row and measure fields.\n     */\n    computeSpecificFields(fields) {\n        this.dateFields = [];\n        this.rowFields = [];\n        this.measureFields = [];\n        fields.forEach((field) => {\n            if (field.store) {\n                // date fields\n                if (field.type === \"date\" || field.type === \"datetime\") {\n                    this.dateFields.push(field);\n                }\n                // row fields\n                if (this.constructor.GROUPABLE_TYPES.includes(field.type)) {\n                    this.rowFields.push(field);\n                }\n                // measure fields\n                if (this.constructor.MEASURABLE_TYPES.includes(field.type)) {\n                    // id and sequence are not measurable\n                    if (field.name !== \"id\" && field.name !== \"sequence\") {\n                        this.measureFields.push(field);\n                    }\n                }\n            }\n        });\n        if (this.dateFields.length) {\n            this.fieldsChoice.date_start = this.dateFields[0].name;\n            this.fieldsChoice.date_stop = this.dateFields[0].name;\n        }\n    }\n\n    async _confirm() {\n        await rpc(\"/web_studio/create_default_view\", {\n            model: this.studio.editedAction.res_model,\n            view_type: this.viewType,\n            attrs: this.fieldsChoice,\n            context: user.context,\n        });\n        super._confirm();\n    }\n}\ndelete NewViewDialog.props.body;\n", "/** @odoo-module **/\n\nimport { ActionContainer } from \"@web/webclient/actions/action_container\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { Component, markup, onWillStart, onWillUnmount, onWillUpdateProps, xml } from \"@odoo/owl\";\nimport { useStudioServiceAsReactive } from \"@web_studio/studio_service\";\nimport { resetViewCompilerCache } from \"@web/views/view_compiler\";\n\nconst editorTabRegistry = registry.category(\"web_studio.editor_tabs\");\n\nexport class StudioActionContainer extends Component {\n    static props = {\n        ...ActionContainer.props,\n        reloadId: { type: Number },\n    };\n\n    static template = xml`\n        <t t-name=\"web.ActionContainer\">\n        <div class=\"o_action_manager\">\n            <t t-if=\"info.Component\" t-component=\"info.Component\" className=\"'o_action'\" t-props=\"info.componentProps\" t-key=\"info.id\"/>\n        </div>\n        </t>`;\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.studio = useStudioServiceAsReactive();\n        this.info = {};\n\n        let actionKey = 1;\n        const onUiUpdate = ({ detail: info }) => {\n            this.info = info;\n            actionKey++;\n            this.render();\n        };\n        this.env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", onUiUpdate);\n        onWillUnmount(() => this.env.bus.removeEventListener(\"ACTION_MANAGER:UPDATE\", onUiUpdate));\n\n        const doAction = async (action, options) => {\n            try {\n                await this.actionService.doAction(action, options);\n                this.actionKey = actionKey;\n            } catch (e) {\n                if (action !== \"web_studio.action_editor\") {\n                    // Fallback on the actionEditor, except if the actionEditor crashes\n                    this.studio.setParams({ editorTab: \"views\" });\n                }\n                // Rethrow anyway: the error doesn't originates from a user's action\n                throw e;\n            }\n        };\n\n        onWillStart(async () => {\n            const action = await this.getStudioAction();\n            this.studioKey = this.studio.requestId;\n            doAction(action);\n            await Promise.resolve();\n        });\n\n        const willUpdateKeepLast = new KeepLast();\n        onWillUpdateProps(async () => {\n            if (this.studio.requestId !== this.studioKey || this.actionKey !== actionKey) {\n                const action = await willUpdateKeepLast.add(this.getStudioAction());\n                resetViewCompilerCache();\n                return new Promise((_resolve) => {\n                    const resolve = () => {\n                        this.env.bus.removeEventListener(\"ACTION_MANAGER:UPDATE\", resolve);\n                        _resolve();\n                    };\n                    this.studioKey = this.studio.requestId;\n                    doAction(action, { clearBreadcrumbs: true, noEmptyTransition: true }).finally(\n                        () => {\n                            this.env.bus.removeEventListener(\"ACTION_MANAGER:UPDATE\", resolve);\n                        }\n                    );\n                    this.env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", resolve);\n                });\n            }\n        });\n    }\n    async getStudioAction() {\n        const { editorTab, editedAction, editedReport, editedViewType } = this.studio;\n        const tab = editorTabRegistry.get(editorTab);\n        if (editorTab === \"views\") {\n            if (editedViewType) {\n                return \"web_studio.view_editor\";\n            }\n            return tab.action;\n        }\n        if (tab.action) {\n            const action = tab.action;\n            return action instanceof Function ? action(this.env) : action;\n        } else if (editorTab === \"reports\" && editedReport) {\n            return \"web_studio.report_editor\";\n        } else {\n            const action = await rpc(\"/web_studio/get_studio_action\", {\n                action_name: editorTab,\n                model: editedAction.res_model,\n                view_id: editedAction.view_id && editedAction.view_id[0], // Not sure it is correct or desirable\n            });\n            action.help = action.help && markup(action.help);\n            return action;\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { COLORS, BG_COLORS } from \"@web_studio/utils\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FontAwesomeIconSelector } from \"@web_studio/client_action/components/font_awesome_icon_selector/font_awesome_icon_selector\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport const DEFAULT_ICON = {\n    backgroundColor: BG_COLORS[0],\n    color: COLORS[10],\n    iconClass: \"fa fa-home\",\n    type: \"custom_icon\",\n};\n\n/**\n * Icon creator\n *\n * Component which purpose is to design an app icon. It can be an uploaded image\n * which will be displayed as is, or an icon customized with the help of presets\n * of colors and icon symbols (@see web_studio/static/src/utils for the full list of colors\n * and icon classes).\n * @extends Component\n */\nexport class IconCreator extends Component {\n    static components = {\n        Dropdown,\n        FileInput,\n        FontAwesomeIconSelector,\n        SelectMenu,\n    };\n    static defaultProps = DEFAULT_ICON;\n    static props = {\n        backgroundColor: { type: String, optional: 1 },\n        color: { type: String, optional: 1 },\n        editable: { type: Boolean, optional: 1 },\n        iconClass: { type: String, optional: 1 },\n        type: { validate: (t) => [\"base64\", \"custom_icon\"].includes(t), optional: 1 },\n        uploaded_attachment_id: { type: Number, optional: 1 },\n        webIconData: { type: String, optional: 1 },\n        onIconChange: Function,\n    };\n    static template = \"web_studio.IconCreator\";\n\n    /**\n     * @param {Object} [props]\n     * @param {string} [props.backgroundColor] Background color of the custom\n     *      icon.\n     * @param {string} [props.color] Color of the custom icon.\n     * @param {boolean} props.editable\n     * @param {string} [props.iconClass] Font Awesome class of the custom icon.\n     * @param {string} props.type 'base64' (if an actual image) or 'custom_icon'.\n     * @param {number} [props.uploaded_attachment_id] Databse ID of an uploaded\n     *      attachment\n     * @param {string} [props.webIconData] Base64-encoded string representing\n     *      the icon image.\n     */\n    setup() {\n        this.orm = useService(\"orm\");\n\n        this.fileInputProps = {\n            acceptedFileExtensions: \"image/png\",\n            resModel: \"res.users\",\n            resId: user.userId,\n        };\n    }\n\n    get backgroundColorChoices() {\n        return this.getChoices(BG_COLORS);\n    }\n\n    get colorChoices() {\n        return this.getChoices(COLORS);\n    }\n\n    getChoices(object) {\n        return object.map((color) => {\n            return {\n                label: color,\n                value: color,\n            };\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onDesignIconClick() {\n        this.props.onIconChange(DEFAULT_ICON);\n    }\n\n    /**\n     * @param {Object[]} files\n     */\n    async onFileUploaded([file]) {\n        if (!file) {\n            // Happens when cancelling upload\n            return;\n        }\n        const res = await this.orm.read(\"ir.attachment\", [file.id], [\"datas\"]);\n\n        this.props.onIconChange({\n            type: \"base64\",\n            uploaded_attachment_id: file.id,\n            webIconData: \"data:image/png;base64,\" + res[0].datas.replace(/\\s/g, \"\"),\n        });\n    }\n\n    /**\n     * @param {string} palette\n     * @param {string} value\n     */\n    onPaletteItemClick(palette, value) {\n        if (this.props[palette] === value) {\n            return; // same value\n        }\n        this.props.onIconChange({\n            backgroundColor: this.props.backgroundColor,\n            color: this.props.color,\n            iconClass: this.props.iconClass,\n            type: \"custom_icon\",\n            [palette]: value,\n        });\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { useDialogConfirmation } from \"@web_studio/client_action/utils\";\nimport { ModelConfiguratorDialog } from \"../model_configurator/model_configurator\";\nimport { RecordSelector } from \"@web/core/record_selectors/record_selector\";\n\nexport class MenuCreatorModel {\n    constructor({ allowNoModel } = {}) {\n        this.data = {\n            modelId: false,\n            menuName: \"\",\n            modelChoice: \"new\",\n        };\n\n        // Info to select what kind of model is linked to the menu\n        this.modelChoiceSelection = {\n            new: _t(\"New Model\"),\n            existing: _t(\"Existing Model\"),\n        };\n\n        if (allowNoModel) {\n            this.modelChoiceSelection.parent = _t(\"Parent Menu\");\n        }\n    }\n\n    validateField(fieldName) {\n        if (fieldName === \"menuName\") {\n            return !!this.data.menuName;\n        } else if (fieldName === \"modelId\") {\n            return this.data.modelChoice === \"existing\" ? !!this.data.modelId : true;\n        }\n    }\n\n    get isValid() {\n        return [\"menuName\", \"modelId\"].every((fName) => this.validateField(fName));\n    }\n}\n\nexport class MenuCreator extends Component {\n    static template = \"web_studio.MenuCreator\";\n    static components = { RecordSelector };\n    static props = {\n        menuCreatorModel: { type: Object },\n        showValidation: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showValidation: false,\n    };\n\n    get multiRecordSelectorProps() {\n        return {\n            resModel: \"ir.model\",\n            resId: this.state.data.modelId && this.state.data.modelId[0],\n            update: (resId) => (this.state.data.modelId = [resId]),\n            domain: [\n                [\"transient\", \"=\", false],\n                [\"abstract\", \"=\", false],\n            ],\n        };\n    }\n\n    setup() {\n        this.state = useState(this.props.menuCreatorModel);\n    }\n\n    isValid(fieldName) {\n        return this.props.showValidation ? this.state.validateField(fieldName) : true;\n    }\n}\n\nexport class MenuCreatorDialog extends Component {\n    static template = \"web_studio.MenuCreatorDialog\";\n    static components = { Dialog, MenuCreator };\n    static props = { confirm: { type: Function }, close: { type: Function } };\n\n    setup() {\n        this.addDialog = useOwnedDialogs();\n        this.menuCreatorModel = useState(new MenuCreatorModel({ allowNoModel: true }));\n        this.state = useState({ showValidation: false });\n        const { confirm, cancel } = useDialogConfirmation({\n            confirm: async (data = {}) => {\n                if (!this.menuCreatorModel.isValid) {\n                    this.state.showValidation = true;\n                    return false;\n                }\n                await this.props.confirm(data);\n            },\n        });\n        this._confirm = confirm;\n        this._cancel = cancel;\n    }\n\n    confirm(data = {}) {\n        this._confirm({ ...this.menuCreatorModel.data, ...data });\n    }\n\n    onCreateNewModel() {\n        if (!this.menuCreatorModel.isValid) {\n            this.state.showValidation = true;\n            return;\n        }\n        this.addDialog(ModelConfiguratorDialog, {\n            confirmLabel: _t(\"Create Menu\"),\n            confirm: (data) => {\n                this.confirm({ modelOptions: data });\n            },\n        });\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { session } from \"@web/session\";\n\n/** You might wonder why I defined all these strings here and not in the template.\n * The reason is that I wanted clear templates that use a single element to render an option,\n * meaning that the label and helper text had to be defined here in the code.\n */\nfunction getModelOptions() {\n    const modelOptions = {\n        use_partner: {\n            label: _t(\"Contact details\"),\n            help: _t(\"Get contact, phone and email fields on records\"),\n            value: false,\n        },\n        use_responsible: {\n            label: _t(\"User assignment\"),\n            help: _t(\"Assign a responsible to each record\"),\n            value: false,\n        },\n        use_date: {\n            label: _t(\"Date & Calendar\"),\n            help: _t(\"Assign dates and visualize records in a calendar\"),\n            value: false,\n        },\n        use_double_dates: {\n            label: _t(\"Date range & Gantt\"),\n            help: _t(\"Define start/end dates and visualize records in a Gantt chart\"),\n            value: false,\n        },\n        use_stages: {\n            label: _t(\"Pipeline stages\"),\n            help: _t(\"Stage and visualize records in a custom pipeline\"),\n            value: false,\n        },\n        use_tags: {\n            label: _t(\"Tags\"),\n            help: _t(\"Categorize records with custom tags\"),\n            value: false,\n        },\n        use_image: {\n            label: _t(\"Picture\"),\n            help: _t(\"Attach a picture to a record\"),\n            value: false,\n        },\n        lines: {\n            label: _t(\"Lines\"),\n            help: _t(\"Add details to your records with an embedded list view\"),\n            value: false,\n        },\n        use_notes: {\n            label: _t(\"Notes\"),\n            help: _t(\"Write additional notes or comments\"),\n            value: false,\n        },\n        use_value: {\n            label: _t(\"Monetary value\"),\n            help: _t(\"Set a price or cost on records\"),\n            value: false,\n        },\n        use_company: {\n            label: _t(\"Company\"),\n            help: _t(\"Restrict a record to a specific company\"),\n            value: false,\n        },\n        use_sequence: {\n            label: _t(\"Custom Sorting\"),\n            help: _t(\"Manually sort records in the list view\"),\n            value: true,\n        },\n        use_mail: {\n            label: _t(\"Chatter\"),\n            help: _t(\"Send messages, log notes and schedule activities\"),\n            value: true,\n        },\n        use_active: {\n            label: _t(\"Archiving\"),\n            help: _t(\"Archive deprecated records\"),\n            value: true,\n        },\n    };\n    if (!session.display_switch_company_menu) {\n        delete modelOptions.use_company;\n    }\n    return modelOptions;\n}\n\nexport class ModelConfigurator extends Component {\n    static template = \"web_studio.ModelConfigurator\";\n    static components = {};\n    static props = {\n        embed: { type: Boolean, optional: true },\n        label: { type: String },\n        onConfirmOptions: Function,\n        onPrevious: Function,\n    };\n\n    setup() {\n        this.state = useState({ saving: false });\n        this.options = useState(getModelOptions());\n    }\n\n    /**\n     * Handle the confirmation of the dialog, just fires an event\n     * to whoever instanciated it.\n     */\n    async onConfirm() {\n        try {\n            this.state.saving = true;\n\n            const mappedOptions = Object.entries(this.options)\n                .filter((opt) => opt[1].value)\n                .map((opt) => opt[0]);\n\n            await this.props.onConfirmOptions(mappedOptions);\n        } finally {\n            this.state.saving = false;\n        }\n    }\n}\n\nexport class ModelConfiguratorDialog extends Component {\n    static components = { Dialog, ModelConfigurator };\n    static template = \"web_studio.ModelConfiguratorDialog\";\n\n    static props = {\n        confirm: { type: Function },\n        close: { type: Function },\n        confirmLabel: { type: String, optional: true },\n    };\n\n    async onConfirm(data) {\n        await this.props.confirm(data);\n        this.props.close();\n    }\n\n    onPrevious() {\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class HomeMenuCustomizer extends Component {\n    static template = \"web_studio.HomeMenuCustomizer\";\n    static props = {};\n    static components = { FileInput };\n\n    setup() {\n        this.ui = useService(\"ui\");\n        this.notification = useService(\"notification\");\n        this.company = useService(\"company\");\n        this.actionManager = useService(\"action\");\n        this.menus = useService(\"menu\");\n        this.dialogManager = useService(\"dialog\");\n    }\n\n    setBackgroundImage(attachment_id) {\n        return rpc(\"/web_studio/set_background_image\", {\n            attachment_id: attachment_id,\n            context: user.context,\n        });\n    }\n    /**\n     * Export all customizations done by Studio in a zip file containing Odoo\n     * modules.\n     */\n    exportCusto() {\n        this.actionManager.doAction(\"web_studio.action_studio_export_wizard\");\n    }\n    /**\n     * Open a dialog allowing to import new modules\n     * (e.g. exported customizations).\n     */\n    importCusto() {\n        const action = {\n            name: \"Import modules\",\n            res_model: \"base.import.module\",\n            views: [[false, \"form\"]],\n            type: \"ir.actions.act_window\",\n            target: \"new\",\n            context: {\n                dialog_size: \"medium\",\n            },\n        };\n        const options = {\n            onClose: () => this.menus.reload(),\n        };\n        this.actionManager.doAction(action, options);\n    }\n\n    async confirmReset() {\n        this.ui.block();\n        try {\n            await rpc(\"/web_studio/reset_background_image\", {\n                context: user.context,\n            });\n            browser.location.reload();\n        } finally {\n            this.ui.unblock();\n        }\n    }\n\n    resetBackground() {\n        this.dialogManager.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to reset the background image?\"),\n            title: _t(\"Confirmation\"),\n            confirm: () => this.confirmReset(),\n        });\n    }\n\n    async onBackgroundUpload([file]) {\n        if (!file) {\n            this.notification.add(_t(\"Could not change the background\"), {\n                sticky: true,\n                type: \"warning\",\n            });\n        } else {\n            this.ui.block();\n            try {\n                await this.setBackgroundImage(file.id);\n                browser.location.reload();\n            } finally {\n                this.ui.unblock();\n            }\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { onMounted, onWillUnmount } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { EnterpriseNavBar } from \"@web_enterprise/webclient/navbar/navbar\";\nimport { HomeMenuCustomizer } from \"./home_menu_customizer/home_menu_customizer\";\nimport { useStudioServiceAsReactive, NotEditableActionError } from \"@web_studio/studio_service\";\n\nconst menuButtonsRegistry = registry.category(\"studio_navbar_menubuttons\");\nexport class StudioNavbar extends EnterpriseNavBar {\n    static template = \"web_studio.StudioNavbar\";\n    static components = {\n        ...EnterpriseNavBar.components,\n        HomeMenuCustomizer,\n    };\n    setup() {\n        super.setup();\n        this.studio = useStudioServiceAsReactive();\n        this.actionManager = useService(\"action\");\n        this.dialogManager = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        onMounted(() => {\n            this.env.bus.removeEventListener(\"HOME-MENU:TOGGLED\", this._busToggledCallback);\n            this._updateMenuAppsIcon();\n        });\n\n        const onMenuButtonsUpdate = () => this.render();\n        menuButtonsRegistry.addEventListener(\"UPDATE\", onMenuButtonsUpdate);\n        onWillUnmount(() => menuButtonsRegistry.removeEventListener(\"UPDATE\", onMenuButtonsUpdate));\n    }\n    onMenuToggle() {\n        this.studio.toggleHomeMenu();\n    }\n    closeStudio() {\n        this.studio.leave();\n    }\n    async onNavBarDropdownItemSelection(menu) {\n        if (menu.actionID) {\n            try {\n                await this.studio.open(this.studio.MODES.EDITOR, menu.actionID);\n            } catch (e) {\n                if (e instanceof NotEditableActionError) {\n                    const options = { type: \"danger\" };\n                    this.notification.add(_t(\"This action is not editable by Studio\"), options);\n                    return;\n                }\n                throw e;\n            }\n        }\n    }\n    get hasBackgroundAction() {\n        return this.studio.editedAction || this.studio.MODES.APP_CREATOR === this.studio.mode;\n    }\n    get isInApp() {\n        return this.studio.mode === this.studio.MODES.EDITOR;\n    }\n    get menuButtons() {\n        return Object.fromEntries(menuButtonsRegistry.getEntries());\n    }\n    _updateMenuAppsIcon() {\n        super._updateMenuAppsIcon();\n        const menuAppsEl = this.menuAppsRef.el;\n        menuAppsEl.classList.toggle(\n            \"o_menu_toggle_studio_app_creator_back\",\n            this.studio.MODES.APP_CREATOR === this.studio.mode\n        );\n    }\n}\n", "/** @odoo-module */\nimport { Component, onWillRender, useState } from \"@odoo/owl\";\nimport { stateToUrl } from \"@web/core/browser/router\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { humanReadableError } from \"@web_studio/client_action/report_editor/utils\";\n\nexport class ErrorDisplay extends Component {\n    static template = \"web_studio.ErrorDisplay\";\n    static props = { error: Object };\n\n    setup() {\n        this.state = useState({ showTrace: false });\n        this.action = useService(\"action\");\n        onWillRender(() => {\n            this.error = humanReadableError(this.props.error);\n        });\n    }\n    openRecord(resModel, resId) {\n        const action = {\n            type: \"ir.actions.act_window\",\n            target: \"new\",\n            res_model: resModel,\n            res_id: resId,\n            views: [[false, \"form\"]],\n            context: {\n                studio: \"0\",\n            },\n        };\n        this.action.doAction(action);\n    }\n    urlFor(model, resId, viewType = \"form\") {\n        return stateToUrl({ action: \"base.action_ui_view\", model, resId });\n    }\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nimport { useReportEditorModel } from \"@web_studio/client_action/report_editor/report_editor_model\";\nimport { ReportEditorWysiwyg } from \"@web_studio/client_action/report_editor/report_editor_wysiwyg/report_editor_wysiwyg\";\nimport { ReportEditorXml } from \"@web_studio/client_action/report_editor/report_editor_xml/report_editor_xml\";\n\nimport { getCssFromPaperFormat } from \"./utils\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nclass ReportEditor extends Component {\n    static template = \"web_studio.ReportEditor\";\n    static components = { ReportEditorWysiwyg, ReportEditorXml };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.reportEditorModel = useReportEditorModel();\n    }\n\n    get paperFormatStyle() {\n        const {\n            margin_top,\n            margin_left,\n            margin_right,\n            print_page_height,\n            print_page_width,\n            header_spacing,\n        } = this.reportEditorModel.paperFormat;\n        const marginTop = Math.max(0, (margin_top || 0) - (header_spacing || 0));\n        return getCssFromPaperFormat({\n            margin_top: marginTop,\n            margin_left,\n            margin_right,\n            print_page_height,\n            print_page_width,\n        });\n    }\n}\nregistry.category(\"actions\").add(\"web_studio.report_editor\", ReportEditor);\n", "/** @odoo-module */\nimport { Component, useRef, useState } from \"@odoo/owl\";\nimport { getCssFromPaperFormat } from \"@web_studio/client_action/report_editor/utils\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { ErrorDisplay } from \"@web_studio/client_action/report_editor/error_display\";\n\nexport class ReportEditorIframe extends Component {\n    static components = { ErrorDisplay };\n    static template = \"web_studio.ReportEditor.Iframe\";\n    static props = {\n        iframeKey: String,\n        iframeSource: String,\n        onIframeLoaded: Function,\n    };\n\n    setup() {\n        this.reportEditorModel = useState(this.env.reportEditorModel);\n        this.iframeRef = useRef(\"iframeRef\");\n        this.onContainerScroll = useThrottleForAnimation(() => {\n            if (this.iframeRef.el?.contentDocument) {\n                this.iframeRef.el.contentDocument.dispatchEvent(new Event(\"scroll\"));\n            }\n        });\n    }\n\n    get paperFormatStyle() {\n        const {\n            margin_top,\n            margin_left,\n            margin_right,\n            print_page_height,\n            print_page_width,\n            header_spacing,\n        } = this.reportEditorModel.paperFormat;\n        const marginTop = Math.max(0, (margin_top || 0) - (header_spacing || 0));\n        return getCssFromPaperFormat({\n            margin_top: marginTop,\n            margin_left,\n            margin_right,\n            print_page_height,\n            print_page_width,\n        });\n    }\n    get iframeStyle() {\n        const { print_page_height } = this.reportEditorModel.paperFormat;\n        return getCssFromPaperFormat({ print_page_height });\n    }\n\n    get iframeSource() {\n        return this.props.iframeSource;\n    }\n\n    get iframeKey() {\n        return this.reportEditorModel.renderKey + \"_\" + (this.props.iframeKey || \"\");\n    }\n\n    async onIframeLoaded() {\n        await this.resizeIframeContent({ iframeRef: this.iframeRef });\n        this.props.onIframeLoaded({ iframeRef: this.iframeRef });\n    }\n\n    async resizeIframeContent({ iframeRef }) {\n        const paperFormat = this.reportEditorModel.paperFormat;\n        const iframeEl = iframeRef.el;\n        const iframeContent = iframeEl.contentDocument;\n\n        // zoom content from 96 (default browser DPI) to paperformat DPI\n        const zoom = 96 / paperFormat.dpi;\n        Array.from(iframeContent.querySelector(\"main\")?.children || []).forEach((el) => {\n            let sectionZoom = zoom;\n            if (!paperFormat.disable_shrinking) {\n                const { width } = el.getBoundingClientRect();\n                sectionZoom = Math.min(zoom, width / el.scrollWidth);\n            }\n            el.setAttribute(\"oe-origin-style\", el.getAttribute(\"style\") || \"\");\n            el.style.setProperty(\"zoom\", sectionZoom);\n        });\n        // WHY --> so that after the load of the iframe, if there are images,\n        // the iframe height is recomputed to the height of the content images included\n        const computeIframeHeight = () =>\n            (iframeEl.style.height = iframeContent.body.scrollHeight + \"px\");\n        //computeIframeHeight();\n\n        // TODO: it seems that the paperformat doesn't exactly do that\n        // this.$content.find('.header').css({\n        //     'margin-bottom': (this.paperFormat.header_spacing || 0) + 'mm',\n        // });\n        // TODO: won't be pretty if the content is larger than the format\n\n        const footer = iframeContent.querySelector(\".footer\");\n        const footerStyle = footer?.style;\n        if (footerStyle) {\n            const { width } = iframeContent.querySelector(\".page\")?.getBoundingClientRect() || {};\n            if (!footer.hasAttribute(\"oe-origin-style\")) {\n                footer.setAttribute(\"oe-origin-style\", footer.getAttribute(\"style\") || \"\");\n            }\n            if (width) {\n                footerStyle.setProperty(\"width\", `${width}px`);\n            }\n        }\n\n        iframeContent.querySelector(\"html\").style.overflow = \"hidden\";\n\n        // set the size of the iframe\n        const proms = [];\n        Array.from(iframeContent.querySelectorAll(\"img[src]\") || []).forEach((img) => {\n            if (img.complete) {\n                return;\n            }\n            const prom = new Promise((resolve) => {\n                img.onload = resolve;\n            });\n            proms.push(prom);\n        });\n        await Promise.all(proms);\n        computeIframeHeight();\n    }\n}\n", "/** @odoo-module */\nimport { Reactive } from \"@web_studio/client_action/utils\";\nimport {\n    EventBus,\n    markRaw,\n    onWillStart,\n    reactive,\n    toRaw,\n    useEnv,\n    useState,\n    useSubEnv,\n    onWillDestroy,\n} from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useEditorBreadcrumbs } from \"@web_studio/client_action/editor/edition_flow\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\nimport { humanReadableError } from \"@web_studio/client_action/report_editor/utils\";\n\nconst notificationErrorTemplate = \"web_studio.ReportEditor.NotificationError\";\nconst errorQweb = `<html><div>The report could not be rendered due to an error</div><html>`;\n\nexport class ReportEditorModel extends Reactive {\n    constructor({ services, debug }) {\n        super();\n        this.debug = debug;\n        this.bus = markRaw(new EventBus());\n        this.mode = \"wysiwyg\";\n        this.warningMessage = \"\";\n        this._isDirty = false;\n        this._isInEdition = false;\n        this._services = markRaw(services);\n        this._errorMessage = false;\n        this.paperFormat = {\n            margin_top: 0,\n            margin_left: 0,\n            margin_right: 0,\n            print_page_width: 210,\n            print_page_height: 297,\n        };\n        this.reportFields = markRaw({\n            id: { name: \"id\", type: \"number\" },\n            name: { name: \"name\", type: \"char\" },\n            model: { name: \"model\", type: \"char\" },\n            report_name: { name: \"report_name\", type: \"char\" },\n            groups_id: {\n                name: \"groups_id\",\n                type: \"many2many\",\n                relation: \"res.groups\",\n                relatedFields: {\n                    display_name: { type: \"char\" },\n                },\n            },\n            paperformat_id: {\n                name: \"paperformat_id\",\n                type: \"many2one\",\n                relation: \"report.paperformat\",\n            },\n            binding_model_id: { name: \"binding_model_id\", type: \"many2one\", relation: \"ir.model\" },\n            attachment_use: { name: \"attachment_use\", type: \"boolean\" },\n            attachment: { name: \"attachment\", type: \"char\" },\n            // fake field\n            display_in_print_menu: { name: \"display_in_print_menu\", type: \"boolean\" },\n        });\n        this.reportActiveFields = markRaw({\n            id: makeActiveField(),\n            name: makeActiveField(),\n            model: makeActiveField(),\n            report_name: makeActiveField(),\n            groups_id: {\n                ...makeActiveField(),\n                related: {\n                    fields: { display_name: { name: \"display_name\", type: \"char\" } },\n                    activeFields: { display_name: makeActiveField() },\n                },\n            },\n            paperformat_id: makeActiveField(),\n            binding_model_id: makeActiveField(),\n            attachment_use: makeActiveField(),\n            attachment: makeActiveField(),\n            // fake field\n            display_in_print_menu: makeActiveField(),\n        });\n        this.reportEnv = {};\n        this.loadHtmlKeepLast = markRaw(new KeepLast());\n\n        this._reportArchs = {};\n        this.renderKey = 1;\n        this.routesContext = pick(user.context, \"allowed_company_ids\");\n    }\n\n    get reportData() {\n        return this._reportChanges || this._reportData;\n    }\n\n    set reportData(_data) {\n        const fields = this.reportFields;\n        const data = { ..._data };\n        for (const [fName, value] of Object.entries(data)) {\n            const field = fields[fName];\n            if (field.type === \"many2many\") {\n                data[fName] = [...value.currentIds];\n            }\n        }\n        this._reportChanges = data;\n    }\n\n    get reportResModel() {\n        return this._reportData.model;\n    }\n\n    get recordToDisplay() {\n        return this.reportEnv.currentId || this.reportEnv.ids.find((i) => !!i) || false;\n    }\n\n    get editedReportId() {\n        return this._services.studio.editedReport.res_id;\n    }\n\n    get reportQweb() {\n        return this._reportArchs.reportQweb;\n    }\n\n    get reportHtml() {\n        return this._reportArchs.reportHtml;\n    }\n\n    get isDirty() {\n        return this._reportChanges || this._isDirty;\n    }\n\n    set isDirty(bool) {\n        this._isDirty = bool;\n    }\n\n    get isInEdition() {\n        return this._isInEdition;\n    }\n\n    get fullErrorDisplay() {\n        return this.debug ? this._errorMessage : false;\n    }\n\n    setInEdition(value) {\n        // Reactivity limitation: if we used a setter, the reactivity will trigger the getter\n        // thus subscribing us to the key. This is not what we want here.\n        value = !!value; // enforce boolean\n        if (reactive(this)._isInEdition === value) {\n            return;\n        }\n        this._isInEdition = value;\n        if (value) {\n            this._services.ui.block();\n        } else {\n            this._services.ui.unblock();\n        }\n    }\n\n    _resetInternalArchs() {\n        // We do this by explicitly bypassing reactivity, we don't want any re-render doing this.\n        // _reportsArchs acts as flag, meaning that if one of the arch is not present\n        // the relevant function will fetch them. see @loadReportQweb and @loadReportHtml\n        toRaw(this)._reportArchs = {};\n    }\n\n    async loadReportEditor() {\n        const loaders = [this.loadReportData.bind(this), this.loadModelEnv.bind(this)];\n        for (const loader of loaders) {\n            if (this.isDestroyed) {\n                break;\n            }\n            await loader();\n        }\n    }\n\n    async loadReportData() {\n        // FIXME introduce alive?\n        const data = await rpc(\"/web_studio/load_report_editor\", {\n            report_id: this.editedReportId,\n            fields: Object.keys(omit(this.reportActiveFields, \"display_in_print_menu\")),\n            context: this.routesContext,\n        });\n        this._reportData = this._parseFakeFields(data.report_data);\n        Object.assign(this.paperFormat, data.paperformat);\n\n        this._errorMessage = data.qweb_error;\n        this._reportArchs.reportQweb = data.report_qweb || errorQweb;\n        this._isLoaded = true;\n    }\n\n    async loadReportQweb() {\n        if (!this._isLoaded) {\n            return;\n        }\n        if (this._reportArchs.reportQweb) {\n            return;\n        }\n\n        try {\n            const reportQweb = await this.loadHtmlKeepLast.add(\n                rpc(\"/web_studio/get_report_qweb\", {\n                    report_id: this.editedReportId,\n                    context: this.routesContext,\n                })\n            );\n            this._errorMessage = false;\n            this._reportArchs.reportQweb = reportQweb;\n        } catch (e) {\n            this._errorMessage = e;\n            this._reportArchs.reportQweb = errorQweb;\n        }\n        this.setInEdition(false);\n    }\n\n    async loadReportHtml({ resId } = {}) {\n        if (!this._isLoaded) {\n            return;\n        }\n        if (resId === undefined && this._reportArchs.reportHtml) {\n            return;\n        }\n        this.reportEnv.currentId = resId !== undefined ? resId : this.reportEnv.currentId;\n        try {\n            const reportHtml = await this.loadHtmlKeepLast.add(\n                rpc(\"/web_studio/get_report_html\", {\n                    report_id: this.editedReportId,\n                    record_id: this.reportEnv.currentId || 0,\n                    context: this.routesContext,\n                })\n            );\n            this._errorMessage = false;\n            this._reportArchs.reportHtml = reportHtml;\n        } catch (e) {\n            this._errorMessage = e;\n            this._reportArchs.reportHtml = errorQweb;\n        }\n        this.setInEdition(false);\n    }\n\n    async saveReport({ htmlParts, urgent, xmlVerbatim } = {}) {\n        const hasPartsToSave = htmlParts && Object.keys(htmlParts).length;\n        const hasVerbatimToSave = xmlVerbatim && Object.keys(xmlVerbatim).length;\n        const hasDataToSave = this.isDirty;\n        this.warningMessage = \"\";\n        if (hasVerbatimToSave && hasPartsToSave) {\n            throw new Error(_t(\"Saving both some report's parts and full xml is not permitted.\"));\n        }\n        if (this._errorMessage && hasPartsToSave) {\n            throw new Error(\n                _t(\"The report is in error. Only editing the XML sources is permitted\")\n            );\n        }\n        if (!hasVerbatimToSave && !hasPartsToSave && !hasDataToSave) {\n            return;\n        }\n        if (!urgent) {\n            this.setInEdition(true);\n        }\n\n        let result;\n        try {\n            result = await rpc(\n                \"/web_studio/save_report\",\n                {\n                    report_id: this.editedReportId,\n                    report_changes: this._reportChanges || null,\n                    html_parts: htmlParts || null,\n                    xml_verbatim: xmlVerbatim || null,\n                    record_id: this.reportEnv.currentId || null,\n                    context: this.routesContext,\n                },\n                { silent: urgent }\n            );\n            this._errorMessage = false;\n        } catch (e) {\n            this.setInEdition(false);\n            const message = renderToMarkup(notificationErrorTemplate, {\n                reportName: this._reportData.name,\n                recordId: this.reportEnv.currentId,\n                error: humanReadableError(e),\n            });\n            this._services.unProtectedNotification.add(message, {\n                type: \"warning\",\n                title: _t(\"Report edition failed\"),\n            });\n            this.warningMessage = _t(\"Report edition failed\");\n\n            if (this._errorMessage) {\n                this._errorMessage = e;\n            }\n\n            return false;\n        }\n\n        if (hasPartsToSave || hasVerbatimToSave) {\n            this._resetInternalArchs();\n        }\n        const { report_data, paperformat, report_html, report_qweb } = result || {};\n        if (!urgent && report_data) {\n            this._reportData = this._parseFakeFields(report_data);\n            this._reportChanges = null;\n            this.paperFormat = paperformat;\n        }\n\n        this.isDirty = false;\n        if (!urgent) {\n            this._reportArchs.reportHtml = report_html;\n            this._reportArchs.reportQweb = report_qweb;\n        }\n        this.setInEdition(false);\n    }\n\n    discardReport() {\n        this.setInEdition(true);\n        this.warningMessage = \"\";\n        this.isDirty = false;\n        this.renderKey++;\n    }\n\n    /**\n     * Load and set the report environment.\n     *\n     * If the report is associated to the same model as the Studio action, the\n     * action ids will be used ; otherwise a search on the report model will be\n     * performed.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    async loadModelEnv() {\n        if (this.reportEnv.ids) {\n            return;\n        }\n        const modelName = this.reportResModel;\n        const result = await this._services.orm.search(modelName, this.getModelDomain(), {\n            context: user.context,\n        });\n\n        this.reportEnv = {\n            ids: result,\n            currentId: result[0] || false,\n        };\n    }\n\n    getModelDomain() {\n        // TODO: Since 13.0, journal entries are also considered as 'account.move',\n        // therefore must filter result to remove them; otherwise not possible\n        // to print invoices and hard to lookup for them if lot of journal entries.\n        const modelName = this.reportResModel;\n        let domain = [];\n        if (modelName === \"account.move\") {\n            domain = [[\"move_type\", \"!=\", \"entry\"]];\n        }\n        return domain;\n    }\n\n    async resetReport(includeHeaderFooter = true) {\n        this.setInEdition(true);\n        await rpc(\"/web_studio/reset_report_archs\", {\n            report_id: this.editedReportId,\n            include_web_layout: includeHeaderFooter,\n        });\n\n        this._resetInternalArchs();\n        await this.loadReportQweb();\n    }\n\n    _parseFakeFields(reportData) {\n        reportData.display_in_print_menu = !!reportData.binding_model_id;\n        return reportData;\n    }\n}\n\nexport function useReportEditorModel() {\n    const services = Object.fromEntries(\n        [\"orm\", \"ui\"].map((name) => {\n            return [name, useService(name)];\n        })\n    );\n    const env = useEnv();\n    services.studio = { ...env.services.studio };\n    services.unProtectedNotification = env.services.notification;\n    const reportEditorModel = new ReportEditorModel({ services, debug: env.debug });\n    useSubEnv({ reportEditorModel });\n\n    function getName(rem) {\n        return rem.reportData?.name;\n    }\n    const crumb = reactive({});\n    const rem = reactive(reportEditorModel, () => {\n        crumb.name = getName(rem);\n    });\n    crumb.name = getName(rem);\n    useEditorBreadcrumbs(crumb);\n\n    onWillStart(() => reportEditorModel.loadReportEditor());\n    onWillDestroy(() => reportEditorModel.isDestroyed = true);\n\n    return useState(reportEditorModel);\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\n\nexport class ReportEditorSnackbar extends Component {\n    static template = \"web_studio.ReportEditor.SnackBar\";\n    static props = {\n        onSave: Function,\n        state: Object,\n        onDiscard: { type: Function, optional: true },\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { Component, reactive } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { visitNode } from \"../utils\";\n\n/**\n * @typedef {Object} CellInfo\n * @property {number} length\n * @property {number} cellIndex\n */\n\nclass TableMenu extends Component {\n    static template = \"html_editor.TableMenu\";\n    static props = {\n        type: String, // column or row\n        overlay: Object,\n        dropdownState: Object,\n        target: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        editable: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        addColumn: Function,\n        removeColumn: Function,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.items = this.props.type === \"column\" ? this.colItems() : [];\n    }\n\n    colItems() {\n        return [\n            {\n                name: \"insert_left\",\n                icon: \"fa-plus\",\n                text: _t(\"Insert left\"),\n                action: this.insertColumn.bind(this, \"before\"),\n            },\n            {\n                name: \"insert_right\",\n                icon: \"fa-plus\",\n                text: _t(\"Insert right\"),\n                action: this.insertColumn.bind(this, \"after\"),\n            },\n            {\n                name: \"delete\",\n                icon: \"fa-trash\",\n                text: _t(\"Delete\"),\n                action: this.deleteColumn.bind(this),\n            },\n        ];\n    }\n\n    insertColumn(position, target) {\n        this.props.addColumn({ position, reference: target });\n        this.props.editable.focus();\n    }\n\n    deleteColumn(target) {\n        this.props.removeColumn({ cell: target });\n        this.props.editable.focus();\n    }\n    onSelected(item) {\n        item.action(this.props.target);\n        this.props.overlay.close();\n    }\n}\n\nfunction setColspan(cell, colspan) {\n    if (colspan < 1) {\n        return;\n    }\n    if (colspan === 1) {\n        cell.removeAttribute(\"colspan\");\n    } else {\n        cell.setAttribute(\"colspan\", `${colspan}`);\n    }\n}\n\nfunction setStyleProperty(element, name, value) {\n    const style = element.style;\n    if (value === undefined) {\n        style.removeProperty(name);\n        if (!style.length) {\n            element.removeAttribute(\"style\");\n        }\n    } else {\n        style.setProperty(name, value);\n    }\n}\n\nfunction* getAllConditionalBlocks(el) {\n    const previousBlocks = [...iterConditionalSiblings(el, true)];\n    for (let index = previousBlocks.length - 1; index >= 0; index--) {\n        const _el = previousBlocks[index];\n        if (_el !== el) {\n            yield _el;\n        }\n    }\n    yield* iterConditionalSiblings(el);\n}\n\nfunction* iterConditionalSiblings(el, reverse = false) {\n    const condition = [\"t-if\", \"t-elif\", \"t-else\"].find((attr) => el.hasAttribute(attr));\n    if (!condition) {\n        return;\n    }\n    const next = reverse ? \"previousElementSibling\" : \"nextElementSibling\";\n    let includeTif = reverse ? true : condition === \"t-if\";\n    while (el) {\n        if (el.hasAttribute(\"t-if\")) {\n            if (includeTif && reverse) {\n                yield el;\n                return;\n            }\n            if (!includeTif) {\n                return;\n            }\n            if (includeTif) {\n                yield el;\n                includeTif = false;\n            }\n        } else {\n            yield el;\n        }\n        const sibl = el[next];\n        el =\n            sibl && [\"t-if\", \"t-elif\", \"t-else\"].some((attr) => sibl.hasAttribute(attr))\n                ? sibl\n                : null;\n    }\n}\n\nconst CELL_TAGS = [\"Q-TH\", \"Q-TD\"];\nconst CSS_COL_COUNT_PROP = \"--q-table-col-count\";\nconst CSS_COL_SIZE_PROP = \"--q-cell-col-size\";\nexport class QWebTablePlugin extends Plugin {\n    static id = \"qweb_table_plugin\";\n    static dependencies = [\"baseContainer\", \"overlay\", \"selection\", \"history\"];\n    resources = {\n        clean_for_save_handlers: ({ root }) => this.clean(root),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    setup() {\n        for (const table of this.editable.querySelectorAll(\"q-table\")) {\n            visitNode(table, (node) => {\n                if (node.tagName !== \"T\") {\n                    node.classList.add(\"oe_unbreakable\");\n                }\n                return !CELL_TAGS.includes(node.tagName);\n            });\n        }\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.colMenu = this.dependencies.overlay.createOverlay(\n            TableMenu,\n            {\n                positionOptions: {\n                    position: \"top-fit\",\n                    offsetY: 0,\n                },\n            },\n            { sequence: 30 }\n        );\n\n        this.isMenuOpened = false;\n        const closeMenus = () => {\n            if (this.isMenuOpened) {\n                this.isMenuOpened = false;\n                this.colMenu.close();\n            }\n        };\n        this.addDomListener(this.document, \"scroll\", closeMenus, true);\n        this.addDomListener(this.document, \"pointermove\", this.onMouseMove);\n    }\n\n    onMouseMove(ev) {\n        const target = ev.target;\n        if (this.isMenuOpened) {\n            return;\n        }\n        if (\n            CELL_TAGS.includes(target.tagName) &&\n            target !== this.activeTd &&\n            this.editable.contains(target)\n        ) {\n            if (ev.target.isContentEditable) {\n                this.setActiveTd(target);\n            }\n        } else if (this.activeTd) {\n            const isOverlay = target.closest(\".o-overlay-container\");\n            if (isOverlay) {\n                return;\n            }\n            const parentTd = closestElement(target, (el) => CELL_TAGS.includes(el.tagName));\n            if (!parentTd) {\n                this.setActiveTd(null);\n            }\n        }\n    }\n\n    setActiveTd(td) {\n        this.activeTd = td;\n        this.colMenu.close();\n        if (!td) {\n            return;\n        }\n        const table = closestElement(td, \"q-table\");\n        if (table.querySelector(\"q-tr\").contains(td)) {\n            this.colMenu.open({\n                target: td,\n                props: {\n                    editable: this.editable,\n                    type: \"column\",\n                    overlay: this.colMenu,\n                    target: td,\n                    dropdownState: this.createDropdownState(),\n                    addColumn: this.addColumn.bind(this),\n                    removeColumn: this.removeColumn.bind(this),\n                },\n            });\n        }\n    }\n\n    createDropdownState(menuToClose) {\n        const dropdownState = reactive({\n            isOpen: false,\n            open: () => {\n                dropdownState.isOpen = true;\n                menuToClose?.close();\n                this.isMenuOpened = true;\n            },\n            close: () => {\n                dropdownState.isOpen = false;\n                this.isMenuOpened = false;\n            },\n        });\n        return dropdownState;\n    }\n\n    addColumn(payload) {\n        const insertedCells = this._addColumn(payload);\n        this.dependencies.selection.setCursorStart(insertedCells[0]);\n        this.dependencies.history.addStep();\n    }\n\n    _addColumn({ position, reference }) {\n        const table = closestElement(reference, \"q-table\");\n\n        const { cells } = new TableSizeComputer().compute(table);\n        const { cellIndex, length } = cells.get(reference);\n        const insertedCells = [];\n        const done = new Set();\n        for (const cellEl of cells.keys()) {\n            let target = cellEl;\n            for (const condition of iterConditionalSiblings(target, position === \"before\")) {\n                target = condition;\n            }\n            if (done.has(target)) {\n                continue;\n            }\n            done.add(target);\n            const cellInfo = cells.get(target);\n            if (cellInfo.cellIndex === cellIndex && cellInfo.length === length) {\n                const newTd = this.createElementFrom(cellEl);\n                target.insertAdjacentElement(\n                    position === \"before\" ? \"beforebegin\" : \"afterend\",\n                    newTd\n                );\n                insertedCells.push(newTd);\n                continue;\n            }\n            if (TableSizeComputer.areOverlappingCellInfo(cellInfo, { cellIndex, length })) {\n                const colspan = parseInt(target.getAttribute(\"colspan\") || \"1\") + 1;\n                setColspan(target, colspan);\n                continue;\n            }\n        }\n        this._normalize(table);\n        return insertedCells;\n    }\n\n    clean(element) {\n        const cleanTableNode = (node) => {\n            if (node.tagName === \"Q-TABLE\") {\n                setStyleProperty(node, CSS_COL_COUNT_PROP, undefined);\n            }\n            const isCell = CELL_TAGS.includes(node.tagName);\n            if (isCell) {\n                setStyleProperty(node, CSS_COL_SIZE_PROP, undefined);\n            }\n            node.classList.remove(\"oe_unbreakable\");\n            if (node.classList.length === 0) {\n                node.removeAttribute(\"class\");\n            }\n            return !isCell;\n        };\n\n        for (const table of element.querySelectorAll(\"q-table\")) {\n            visitNode(table, cleanTableNode);\n        }\n    }\n\n    removeColumn(payload) {\n        const table = closestElement(payload.cell, \"q-table\");\n        this._removeColumn(payload);\n        const firstCell = table.querySelector(CELL_TAGS.join(\",\"));\n        this.dependencies.selection.setCursorEnd(firstCell);\n        this.dependencies.history.addStep();\n    }\n\n    _removeColumn({ cell }) {\n        const table = closestElement(cell, \"q-table\");\n\n        const { cells } = new TableSizeComputer().compute(table);\n        const { cellIndex, length } = cells.get(cell);\n        const removedCells = [];\n        const done = new Set();\n        for (const cellEl of cells.keys()) {\n            if (done.has(cellEl)) {\n                continue;\n            }\n            const cellInfo = cells.get(cellEl);\n\n            if (cellIndex === cellInfo.cellIndex && length === cellInfo.length) {\n                for (const removed of this._removeCell(cellEl)) {\n                    removedCells.push(removed);\n                    done.add(removed);\n                }\n                continue;\n            }\n            if (TableSizeComputer.areOverlappingCellInfo(cellInfo, { length, cellIndex })) {\n                const colspan = parseInt(cellEl.getAttribute(\"colspan\") || \"1\") - 1;\n                if (colspan === 0) {\n                    for (const removed of this._removeCell(cellEl)) {\n                        removedCells.push(removed);\n                        done.add(removed);\n                    }\n                    continue;\n                }\n                setColspan(cellEl, colspan);\n            }\n        }\n        this._normalize(table);\n        return removedCells;\n    }\n\n    _removeCell(cellEl) {\n        const toRemove = [...getAllConditionalBlocks(cellEl)];\n        if (!toRemove.length) {\n            toRemove.push(cellEl);\n        }\n        for (const _el of toRemove) {\n            _el.remove();\n        }\n        return toRemove;\n    }\n\n    createElementFrom(fromElement) {\n        const tagName = fromElement.tagName.toLowerCase();\n        const newElement = this.document.createElement(tagName);\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        baseContainer.append(this.document.createElement(\"br\"));\n        newElement.append(baseContainer);\n        newElement.classList.add(\"oe_unbreakable\");\n        return newElement;\n    }\n\n    normalize(el) {\n        for (const table of el.querySelectorAll(\"q-table\")) {\n            this._normalize(table);\n        }\n    }\n    _normalize(table) {\n        const { cells, baseLineRow } = new TableSizeComputer().compute(table);\n\n        setStyleProperty(table, CSS_COL_COUNT_PROP, `${baseLineRow.length}`);\n        for (const [cellEl, info] of cells.entries()) {\n            setStyleProperty(\n                cellEl,\n                CSS_COL_SIZE_PROP,\n                info.length === 1 ? undefined : `${info.length}`\n            );\n        }\n    }\n}\n\n/**\n * Recursively traverses a table-like tree in order to compute each cell's start position and length (colspan)\n * It stops traversing when encountering a table cell (q-th, q-td)\n * It handles qweb conditions (t-if/t-elif/t-else).\n * The algo goes:\n * - start traversing the table depth first.\n * - when encountering a row (q-tr), reset cellIndex and cellCount to 0\n * - when encountering a cell (q-td, q-th), increment cellIndex and cellCount\n * - don't traverse the cell itself\n * - a cell can have a colspan, increment cellIndex accordingly\n * - when encoutering a t-if: stop temporarly the depth first traversal\n * - store counters\n * - count cells in each mutually exclusive branch\n * - set counters to the maximum yield among the branches\n * - resume normal traversal.\n */\nclass TableSizeComputer {\n    /**\n     * @param {CellInfo} info1\n     * @param {CellInfo} info2\n     */\n    static areOverlappingCellInfo(info1, info2) {\n        if (info1.cellIndex <= info2.cellIndex) {\n            return info1.cellIndex + info1.length > info2.cellIndex;\n        }\n        if (info1.cellIndex > info2.cellIndex) {\n            return info2.cellIndex + info2.length > info1.cellIndex;\n        }\n    }\n\n    compute(table) {\n        this.cells = new Map();\n        this.rows = new Map();\n        this.rowIndex = 0;\n        this.cellIndex = 0;\n        this.cellCount = 0;\n        this.maxCellLength = 0;\n        this.processChildren(table);\n        return {\n            rows: this.rows,\n            cells: this.cells,\n            maxCellLength: this.maxCellLength,\n            baseLineRow: this.baseLineRow,\n        };\n    }\n\n    processCell(cell, params) {\n        const cellLength = parseInt(cell.getAttribute(\"colspan\") || \"1\");\n        const cellIndex = this.cellIndex;\n        this.cellCount++;\n        this.cellIndex = this.cellIndex + cellLength;\n        this.cells.set(cell, { length: cellLength, cellEl: cell, rowEl: params.rowEl, cellIndex });\n    }\n\n    processRow(el, params) {\n        const row = { rowIndex: this.rowIndex++ };\n        this.rows.set(el, row);\n        for (const child of el.children) {\n            this.processElement(child, { ...params, rowEl: el });\n        }\n        if (!this.baseLineRow) {\n            this.baseLineRow = row;\n        }\n        row.length = this.cellIndex;\n        row.cellCount = this.cellCount;\n        this.maxCellLength = Math.max(this.cellIndex, this.maxCellLength);\n        this.cellIndex = 0;\n        this.cellCount = 0;\n    }\n\n    processConditions(el, params) {\n        // we are about to compute the index of elements inside a t-if/t-elif/t-else\n        // sequence of direct siblings.\n        // These siblings are mutually exclusive. Hence, we store the current index,\n        // go inside the branch, then reset the index to its value.\n        // The resulting end index of these siblings in the maximum of them all.\n        const cellIndex = this.cellIndex;\n        let maxCellIndex = cellIndex;\n\n        const cellCount = this.cellCount;\n        let maxCellCount = cellCount;\n\n        for (const conditional of iterConditionalSiblings(el)) {\n            this.processElement(conditional, params, false);\n            maxCellIndex = Math.max(maxCellIndex, this.cellIndex);\n            maxCellCount = Math.max(maxCellCount, this.cellCount);\n            this.cellIndex = cellIndex;\n            this.cellCount = cellCount;\n        }\n        this.cellIndex = maxCellIndex;\n        this.cellCount = maxCellCount;\n    }\n\n    processChildren(el, params = {}) {\n        for (const child of el.children) {\n            this.processElement(child, params);\n        }\n    }\n\n    processElement(el, params = {}, processConditions = true) {\n        if (el.hasAttribute(\"t-set\")) {\n            return;\n        }\n\n        if (processConditions) {\n            const directive = [\"t-if\", \"t-elif\", \"t-else\"].find((attr) => el.hasAttribute(attr));\n            if (directive === \"t-if\") {\n                return this.processConditions(el, params);\n            } else if (directive && el.tagName === \"T\") {\n                // Don't plunge into t-elif/t-else: this has been done already\n                return;\n            }\n        }\n        if (CELL_TAGS.includes(el.tagName)) {\n            return this.processCell(el, params);\n        }\n        if (el.tagName === \"Q-TR\") {\n            return this.processRow(el, params);\n        }\n        return this.processChildren(el, params);\n    }\n}\n", "/** @odoo-module */\nimport {\n    Component,\n    onWillStart,\n    onMounted,\n    onWillDestroy,\n    onWillUnmount,\n    reactive,\n    useState,\n} from \"@odoo/owl\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { ensureJQuery } from \"@web/core/ensure_jquery\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\n\nimport { StudioDynamicPlaceholderPopover } from \"./studio_dynamic_placeholder_popover\";\nimport { Many2ManyTagsField } from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { CharField } from \"@web/views/fields/char/char_field\";\nimport { Record as _Record } from \"@web/model/record\";\nimport { Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { BooleanField } from \"@web/views/fields/boolean/boolean_field\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { ReportEditorSnackbar } from \"@web_studio/client_action/report_editor/report_editor_snackbar\";\nimport { useEditorMenuItem } from \"@web_studio/client_action/editor/edition_flow\";\nimport { memoizeOnce } from \"@web_studio/client_action/utils\";\nimport { ReportEditorIframe } from \"../report_editor_iframe\";\nimport { Editor } from \"@html_editor/editor\";\nimport { MAIN_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { QWebPlugin } from \"@html_editor/others/qweb_plugin\";\nimport { nodeSize } from \"@html_editor/utils/position\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { QWebTablePlugin } from \"./qweb_table_plugin\";\nimport { visitNode } from \"../utils\";\nimport { TablePlugin } from \"@html_editor/main/table/table_plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nclass __Record extends _Record.components._Record {\n    setup() {\n        super.setup();\n        const willSaveUrgently = () => this.model.bus.trigger(\"WILL_SAVE_URGENTLY\");\n        onMounted(() => {\n            this.env.reportEditorModel.bus.addEventListener(\"WILL_SAVE_URGENTLY\", willSaveUrgently);\n        });\n\n        onWillDestroy(() =>\n            this.env.reportEditorModel.bus.removeEventListener(\n                \"WILL_SAVE_URGENTLY\",\n                willSaveUrgently\n            )\n        );\n    }\n}\n\nclass Record extends _Record {\n    static components = { ..._Record.components, _Record: __Record };\n}\n\nfunction getOrderedTAs(node) {\n    const results = [];\n    while (node) {\n        const closest = node.closest(\"[t-foreach]\");\n        if (closest) {\n            results.push(closest.getAttribute(\"t-as\"));\n            node = closest.parentElement;\n        } else {\n            node = null;\n        }\n    }\n    return results;\n}\n\nclass FieldDynamicPlaceholder extends Component {\n    static components = { StudioDynamicPlaceholderPopover, SelectMenu };\n    static template = \"web_studio.FieldDynamicPlaceholder\";\n    static props = {\n        resModel: String,\n        availableQwebVariables: Object,\n        close: Function,\n        validate: Function,\n        isEditingFooterHeader: Boolean,\n        initialQwebVar: { optional: true, type: String },\n        showOnlyX2ManyFields: Boolean,\n    };\n\n    static defaultProps = {\n        initialQwebVar: \"\",\n    };\n\n    setup() {\n        this.state = useState({ currentVar: this.getDefaultVariable() });\n        useHotkey(\"escape\", () => this.props.close());\n    }\n\n    get currentResModel() {\n        const currentVar = this.state.currentVar;\n        const resModel = currentVar && this.props.availableQwebVariables[currentVar].model;\n        return resModel || this.props.resModel;\n    }\n\n    get sortedVariables() {\n        const entries = Object.entries(this.props.availableQwebVariables).filter(\n            ([k, v]) => v.in_foreach && !this.props.isEditingFooterHeader\n        );\n        const resModel = this.props.resModel;\n        const sortFn = ([k, v]) => {\n            let score = 0;\n            if (k === \"doc\") {\n                score += 2;\n            }\n            if (k === \"docs\") {\n                score -= 2;\n            }\n            if (k === \"o\") {\n                score++;\n            }\n            if (v.model === resModel) {\n                score++;\n            }\n            return score;\n        };\n\n        const mapFn = ([k, v]) => {\n            return {\n                value: k,\n                label: `${k} (${v.name})`,\n            };\n        };\n        return sortBy(entries, sortFn, \"desc\").map((e) => mapFn(e));\n    }\n\n    validate(...args) {\n        this.props.validate(this.state.currentVar, ...args);\n    }\n\n    getDefaultVariable() {\n        const initialQwebVar = this.props.initialQwebVar;\n        if (initialQwebVar && initialQwebVar in this.props.availableQwebVariables) {\n            return initialQwebVar;\n        }\n        if (this.props.isEditingFooterHeader) {\n            const companyVar = Object.entries(this.props.availableQwebVariables).find(\n                ([k, v]) => v.model === \"res.company\"\n            );\n            return companyVar && companyVar[0];\n        }\n\n        let defaultVar = this.sortedVariables.find((v) => {\n            return [\"doc\", \"o\"].includes(v.value);\n        });\n        defaultVar =\n            defaultVar ||\n            this.sortedVariables.find(\n                (v) => this.props.availableQwebVariables[v.value].model === this.props.resModel\n            );\n        return defaultVar && defaultVar.value;\n    }\n}\n\nclass UndoRedo extends Component {\n    static template = \"web_studio.ReportEditorWysiwyg.UndoRedo\";\n    static props = {\n        state: Object,\n    };\n}\n\nclass ResetConfirmationPopup extends ConfirmationDialog {\n    static template = \"web_studio.ReportEditorWysiwyg.ResetConfirmationPopup\";\n    static props = {\n        ...omit(ConfirmationDialog.props, \"body\"),\n        state: Object,\n    };\n}\n\nconst CUSTOM_BRANDING_ATTR = [\n    \"ws-view-id\",\n    \"ws-call-key\",\n    \"ws-call-group-key\",\n    \"ws-real-children\",\n    \"o-diff-key\",\n];\n\nclass _TablePlugin extends TablePlugin {\n    static id = TablePlugin.id;\n    _insertTable() {\n        const table = super._insertTable(...arguments);\n        if (closestElement(table, \"[t-call='web.external_layout']\")) {\n            table.removeAttribute(\"class\");\n            table.classList.add(\"table\", \"o_table\", \"table-borderless\");\n        }\n        return table;\n    }\n}\n\nconst REPORT_EDITOR_PLUGINS_MAP = Object.fromEntries(MAIN_PLUGINS.map((cls) => [cls.id, cls]));\nObject.assign(REPORT_EDITOR_PLUGINS_MAP, {\n    [QWebPlugin.id]: QWebPlugin,\n    [QWebTablePlugin.id]: QWebTablePlugin,\n    [TablePlugin.id]: _TablePlugin,\n});\n\nexport class ReportEditorWysiwyg extends Component {\n    static components = {\n        CharField,\n        Record,\n        Many2ManyTagsField,\n        Many2OneField,\n        BooleanField,\n        UndoRedo,\n        ReportEditorIframe,\n    };\n    static props = {\n        paperFormatStyle: String,\n    };\n    static template = \"web_studio.ReportEditorWysiwyg\";\n\n    setup() {\n        this.action = useService(\"action\");\n        this.addDialog = useOwnedDialogs();\n        this.notification = useService(\"notification\");\n\n        this._getReportQweb = memoizeOnce(() => {\n            const tree = new DOMParser().parseFromString(\n                this.reportEditorModel.reportQweb,\n                \"text/html\"\n            );\n            return tree.firstElementChild;\n        });\n\n        const reportEditorModel = (this.reportEditorModel = useState(this.env.reportEditorModel));\n\n        this.fieldPopover = usePopover(FieldDynamicPlaceholder);\n        useEditorMenuItem({\n            component: ReportEditorSnackbar,\n            props: {\n                state: reportEditorModel,\n                onSave: this.save.bind(this),\n                onDiscard: this.discard.bind(this),\n            },\n        });\n\n        // This little reactive is to be bound to the editor, so we create it here.\n        // This could have been a useState, but the current component doesn't use it.\n        // Instead, it passes it to a child of his,\n        this.undoRedoState = reactive({\n            canUndo: false,\n            canRedo: false,\n            undo: () => this.editor?.shared.history.undo(),\n            redo: () => this.editor?.shared.history.redo(),\n        });\n\n        onWillStart(async () => {\n            await ensureJQuery();\n            await Promise.all([\n                loadBundle(\"web_editor.backend_assets_wysiwyg\"),\n                this.reportEditorModel.loadReportQweb(),\n            ]);\n        });\n\n        onWillUnmount(() => {\n            this.reportEditorModel.bus.trigger(\"WILL_SAVE_URGENTLY\");\n            this.save({ urgent: true });\n            if (this.editor) {\n                this.editor.destroy(true);\n            }\n        });\n    }\n\n    instantiateEditor({ editable } = {}) {\n        this.undoRedoState.canUndo = false;\n        this.undoRedoState.canRedo = false;\n        const onEditorChange = () => {\n            const canUndo = this.editor.shared.history.canUndo();\n            this.reportEditorModel.isDirty = canUndo;\n            Object.assign(this.undoRedoState, {\n                canUndo: canUndo,\n                canRedo: this.editor.shared.history.canRedo(),\n            });\n        };\n\n        editable.querySelectorAll(\"[ws-view-id]\").forEach((el) => {\n            el.setAttribute(\"contenteditable\", \"true\");\n        });\n\n        const editor = new Editor(\n            {\n                Plugins: Object.values(REPORT_EDITOR_PLUGINS_MAP),\n                onChange: onEditorChange,\n                getRecordInfo: () => {\n                    const { anchorNode } = this.editor.shared.selection.getEditableSelection();\n                    if (!anchorNode) {\n                        return {};\n                    }\n                    const lastViewParent = closestElement(anchorNode, \"[ws-view-id]\");\n                    if (!lastViewParent) {\n                        return {};\n                    }\n                    return {\n                        resModel: \"ir.ui.view\",\n                        resId: parseInt(lastViewParent.getAttribute(\"ws-view-id\")),\n                        field: \"arch\",\n                    };\n                },\n                resources: {\n                    handleNewRecords: this.handleMutations.bind(this),\n                    powerbox_categories: withSequence(5, {\n                        id: \"report_tools\",\n                        name: _t(\"Report Tools\"),\n                    }),\n                    user_commands: this.getUserCommands(),\n                    powerbox_items: this.getPowerboxCommands(),\n                    unsplittable_node_predicates: (node) => {\n                        return (\n                            node.nodeType === Node.ELEMENT_NODE &&\n                            node.matches(\".page, .header, .footer\")\n                        );\n                    },\n                },\n                disableVideo: true,\n            },\n            this.env.services\n        );\n        editor.attachTo(editable);\n        // disable the qweb's plugin class: its style is too complex and confusing\n        // in the case of reports\n        editable.classList.remove(\"odoo-editor-qweb\");\n        return editor;\n    }\n\n    onIframeLoaded({ iframeRef }) {\n        if (this.editor) {\n            this.editor.destroy(true);\n        }\n        this.iframeRef = iframeRef;\n        const doc = iframeRef.el.contentDocument;\n        doc.body.classList.remove(\"container\");\n\n        if (odoo.debug) {\n            [\"t-esc\", \"t-out\", \"t-field\"].forEach((tAtt) => {\n                doc.querySelectorAll(`*[${tAtt}]`).forEach((e) => {\n                    // Save the previous title to set it back before saving the report\n                    if (e.hasAttribute(\"title\")) {\n                        e.setAttribute(\"data-oe-title\", e.getAttribute(\"title\"));\n                    }\n                    e.setAttribute(\"title\", e.getAttribute(tAtt));\n                });\n            });\n        }\n        if (!this.reportEditorModel._errorMessage) {\n            this.editor = this.instantiateEditor({ editable: doc.querySelector(\"#wrapwrap\") });\n        }\n        this.reportEditorModel.setInEdition(false);\n    }\n\n    handleMutations(records) {\n        for (const record of records) {\n            if (record.type === \"attributes\") {\n                if (record.attributeName === \"contenteditable\") {\n                    continue;\n                }\n                if (record.attributeName.startsWith(\"data-oe-t\")) {\n                    continue;\n                }\n            }\n            if (record.type === \"childList\") {\n                Array.from(record.addedNodes).forEach((el) => {\n                    if (el.nodeType !== 1) {\n                        return;\n                    }\n                    visitNode(el, (node) => {\n                        CUSTOM_BRANDING_ATTR.forEach((attr) => {\n                            node.removeAttribute(attr);\n                        });\n                        node.classList.remove(\"o_dirty\");\n                    });\n                });\n                const realRemoved = [...record.removedNodes].filter(\n                    (n) => n.nodeType !== Node.COMMENT_NODE\n                );\n                if (!realRemoved.length && !record.addedNodes.length) {\n                    continue;\n                }\n            }\n\n            let target = record.target;\n            if (!target.isConnected) {\n                continue;\n            }\n            if (target.nodeType !== Node.ELEMENT_NODE) {\n                target = target.parentElement;\n            }\n            if (!target) {\n                continue;\n            }\n\n            target = target.closest(`[ws-view-id]`);\n            if (!target) {\n                continue;\n            }\n            if (!target.classList.contains(\"o_dirty\")) {\n                target.classList.add(\"o_dirty\");\n            }\n        }\n    }\n\n    get reportQweb() {\n        const model = this.reportEditorModel;\n        return this._getReportQweb(`${model.renderKey}_${model.reportQweb}`).outerHTML;\n    }\n\n    get reportRecordProps() {\n        const model = this.reportEditorModel;\n        return {\n            fields: model.reportFields,\n            activeFields: model.reportActiveFields,\n            values: model.reportData,\n        };\n    }\n\n    async save({ urgent = false } = {}) {\n        if (!this.editor) {\n            await this.reportEditorModel.saveReport({ urgent });\n            return;\n        }\n        const htmlParts = {};\n        const editable = this.editor.getElContent();\n\n        // Clean technical title\n        if (odoo.debug) {\n            editable.querySelectorAll(\"*[t-field],*[t-out],*[t-esc]\").forEach((e) => {\n                if (e.hasAttribute(\"data-oe-title\")) {\n                    e.setAttribute(\"title\", e.getAttribute(\"data-oe-title\"));\n                    e.removeAttribute(\"data-oe-title\");\n                } else {\n                    e.removeAttribute(\"title\");\n                }\n            });\n        }\n\n        editable.querySelectorAll(\"[ws-view-id].o_dirty\").forEach((el) => {\n            el.classList.remove(\"o_dirty\");\n            el.removeAttribute(\"contenteditable\");\n            const viewId = el.getAttribute(\"ws-view-id\");\n            if (!viewId) {\n                return;\n            }\n            Array.from(el.querySelectorAll(\"[t-call]\")).forEach((el) => {\n                el.removeAttribute(\"contenteditable\");\n                el.replaceChildren();\n            });\n\n            Array.from(el.querySelectorAll(\"[oe-origin-t-out]\")).forEach((el) => {\n                el.replaceChildren();\n            });\n            if (!el.hasAttribute(\"oe-origin-class\") && el.getAttribute(\"class\") === \"\") {\n                el.removeAttribute(\"class\");\n            }\n\n            const callGroupKey = el.getAttribute(\"ws-call-group-key\");\n            const type = callGroupKey ? \"in_t_call\" : \"full\";\n\n            const escaped_html = el.outerHTML;\n            htmlParts[viewId] = htmlParts[viewId] || [];\n\n            htmlParts[viewId].push({\n                call_key: el.getAttribute(\"ws-call-key\"),\n                call_group_key: callGroupKey,\n                type,\n                html: escaped_html,\n            });\n        });\n        await this.reportEditorModel.saveReport({ htmlParts, urgent });\n    }\n\n    async discard() {\n        if (this.editor) {\n            const selection = this.editor.document.getSelection();\n            if (selection) {\n                selection.removeAllRanges();\n            }\n        }\n        this.env.services.dialog.add(ConfirmationDialog, {\n            body: _t(\n                \"If you discard the current edits, all unsaved changes will be lost. You can cancel to return to edit mode.\"\n            ),\n            confirm: () => this.reportEditorModel.discardReport(),\n            cancel: () => {},\n        });\n    }\n\n    getUserCommands() {\n        const isAvailable = (selection) => {\n            const { anchorNode } = selection;\n            const { availableQwebVariables } = this.getQwebVariables(\n                anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentElement\n            );\n            return Object.keys(availableQwebVariables || {}).length > 0;\n        };\n        return [\n            {\n                id: \"insertField\",\n                title: _t(\"Field\"),\n                description: _t(\"Insert a field\"),\n                icon: \"fa-magic\",\n                run: this.insertField.bind(this),\n                isAvailable,\n            },\n            {\n                id: \"insertDynamicTable\",\n                title: _t(\"Dynamic Table\"),\n                description: _t(\"Insert a table based on a relational field.\"),\n                icon: \"fa-magic\",\n                run: this.insertTableX2Many.bind(this),\n                isAvailable,\n            },\n        ];\n    }\n\n    getPowerboxCommands() {\n        return [\n            withSequence(20, {\n                categoryId: \"report_tools\",\n                commandId: \"insertField\",\n            }),\n            withSequence(25, {\n                categoryId: \"report_tools\",\n                commandId: \"insertDynamicTable\",\n            }),\n        ];\n    }\n\n    getQwebVariables(element) {\n        if (!element) {\n            return {};\n        }\n        const nodeOeContext = element.closest(\"[oe-context]\");\n        let availableQwebVariables =\n            nodeOeContext && JSON.parse(nodeOeContext.getAttribute(\"oe-context\"));\n\n        const isInHeaderFooter = closestElement(element, \".header,.footer\");\n        let initialQwebVar;\n        if (isInHeaderFooter) {\n            const companyVars = Object.entries(availableQwebVariables).filter(\n                ([k, v]) => v.model === \"res.company\"\n            );\n            initialQwebVar = companyVars[0]?.[0];\n            availableQwebVariables = Object.fromEntries(companyVars);\n        } else {\n            initialQwebVar = getOrderedTAs(element)[0] || \"\";\n        }\n        return {\n            isInHeaderFooter,\n            availableQwebVariables,\n            initialQwebVar,\n        };\n    }\n\n    getFieldPopoverParams() {\n        const resModel = this.reportEditorModel.reportResModel;\n        const odooEditor = this.editor;\n\n        const { anchorNode } = odooEditor.shared.selection.getEditableSelection();\n        const popoverAnchor = anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentElement;\n        const { availableQwebVariables, initialQwebVar, isInHeaderFooter } =\n            this.getQwebVariables(popoverAnchor);\n\n        return {\n            popoverAnchor,\n            props: {\n                availableQwebVariables,\n                initialQwebVar,\n                isEditingFooterHeader: !!isInHeaderFooter,\n                resModel,\n            },\n        };\n    }\n\n    async insertTableX2Many() {\n        const { popoverAnchor, props } = this.getFieldPopoverParams();\n        await this.fieldPopover.open(popoverAnchor, {\n            ...props,\n            showOnlyX2ManyFields: true,\n            validate: (\n                qwebVar,\n                fieldNameChain,\n                defaultValue = \"\",\n                is_image,\n                relation,\n                relationName\n            ) => {\n                const doc = this.editor.document;\n                this.editor.editable.focus();\n\n                const table = doc.createElement(\"table\");\n                table.classList.add(\"table\", \"table-sm\");\n\n                const tBody = table.createTBody();\n\n                const topRow = tBody.insertRow();\n                topRow.classList.add(\n                    \"border-bottom\",\n                    \"border-top-0\",\n                    \"border-start-0\",\n                    \"border-end-0\",\n                    \"border-2\",\n                    \"border-dark\",\n                    \"fw-bold\"\n                );\n                const topTd = doc.createElement(\"td\");\n                topTd.appendChild(doc.createTextNode(defaultValue || \"Column name\"));\n                topRow.appendChild(topTd);\n\n                const tr = doc.createElement(\"tr\");\n                tr.setAttribute(\"t-foreach\", `${qwebVar}.${fieldNameChain}`);\n                tr.setAttribute(\"t-as\", \"x2many_record\");\n                tr.setAttribute(\n                    \"oe-context\",\n                    JSON.stringify({\n                        x2many_record: {\n                            model: relation,\n                            in_foreach: true,\n                            name: relationName,\n                        },\n                        ...props.availableQwebVariables,\n                    })\n                );\n                tBody.appendChild(tr);\n\n                const td = doc.createElement(\"td\");\n                td.textContent = _t(\"Insert a field...\");\n                tr.appendChild(td);\n\n                this.editor.shared.dom.insert(table);\n                this.editor.shared.selection.setSelection({\n                    anchorNode: td,\n                    focusOffset: nodeSize(td),\n                });\n                this.editor.shared.history.addStep();\n            },\n        });\n    }\n\n    async insertField() {\n        const { popoverAnchor, props } = this.getFieldPopoverParams();\n        await this.fieldPopover.open(popoverAnchor, {\n            ...props,\n            showOnlyX2ManyFields: false,\n            validate: (\n                qwebVar,\n                fieldNameChain,\n                defaultValue = \"\",\n                is_image,\n                relation,\n                fieldString\n            ) => {\n                const doc = this.editor.document;\n\n                const span = doc.createElement(\"span\");\n                span.setAttribute(\n                    \"oe-expression-readable\",\n                    fieldString || `field: \"${qwebVar}.${fieldNameChain}\"`\n                );\n                span.textContent = defaultValue;\n                span.setAttribute(\"t-field\", `${qwebVar}.${fieldNameChain}`);\n\n                if (odoo.debug) {\n                    span.setAttribute(\"title\", `${qwebVar}.${fieldNameChain}`);\n                }\n\n                if (is_image) {\n                    span.setAttribute(\"t-options-widget\", \"'image'\");\n                    span.setAttribute(\"t-options-qweb_img_raw_data\", 1);\n                }\n                this.editor.shared.dom.insert(span);\n                this.editor.editable.focus();\n                this.editor.shared.history.addStep();\n            },\n        });\n    }\n\n    async printPreview() {\n        const model = this.reportEditorModel;\n        await this.save();\n        const recordId = model.reportEnv.currentId || model.reportEnv.ids.find((i) => !!i) || false;\n        if (!recordId) {\n            this.notification.add(\n                _t(\n                    \"There is no record on which this report can be previewed. Create at least one record to preview the report.\"\n                ),\n                {\n                    type: \"danger\",\n                    title: _t(\"Report preview not available\"),\n                }\n            );\n            return;\n        }\n\n        const action = await rpc(\"/web_studio/print_report\", {\n            record_id: recordId,\n            report_id: model.editedReportId,\n        });\n        this.reportEditorModel.renderKey++;\n        return this.action.doAction(action, { clearBreadcrumbs: true });\n    }\n\n    async resetReport() {\n        const state = reactive({ includeHeaderFooter: true });\n        this.addDialog(ResetConfirmationPopup, {\n            title: _t(\"Reset report\"),\n            confirmLabel: _t(\"Reset report\"),\n            confirmClass: \"btn-danger\",\n            cancelLabel: _t(\"Go back\"),\n            state,\n            cancel: () => {},\n            confirm: async () => {\n                await this.reportEditorModel.saveReport();\n                try {\n                    await this.reportEditorModel.resetReport(state.includeHeaderFooter);\n                } finally {\n                    this.reportEditorModel.renderKey++;\n                }\n            },\n        });\n    }\n\n    async openReportFormView() {\n        await this.save();\n        return this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"ir.actions.report\",\n                res_id: this.reportEditorModel.editedReportId,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            },\n            { clearBreadcrumbs: true }\n        );\n    }\n\n    async editSources() {\n        await this.save();\n        this.reportEditorModel.mode = \"xml\";\n    }\n}\n", "/** @odoo-module **/\n\nimport { DynamicPlaceholderPopover } from \"@web/views/fields/dynamic_placeholder_popover\";\nimport { useLoadFieldInfo } from \"@web/core/model_field_selector/utils\";\n\nexport class StudioDynamicPlaceholderPopover extends DynamicPlaceholderPopover {\n    static template = \"web_studio.StudioDynamicPlaceholderPopover\";\n    static props = [...DynamicPlaceholderPopover.props, \"showOnlyX2ManyFields\"];\n    setup() {\n        super.setup();\n        this.loadFieldInfo = useLoadFieldInfo();\n    }\n\n    filter(fieldDef) {\n        if (this.props.showOnlyX2ManyFields) {\n            return [\"one2many\", \"many2many\"].includes(fieldDef.type);\n        } else {\n            /**\n             * We don't want to display x2many fields inside a report as it would not make sense.\n             * We also don't want to display boolean fields.\n             * This override is necessary because we want to be able to select non-searchable fields.\n             * There is no reason as to why this wouldn't be allowed inside a report as we don't search on those fields,\n             * we simply render them.\n             */\n            return ![\"one2many\", \"boolean\", \"many2many\"].includes(fieldDef.type);\n        }\n    }\n\n    async validate() {\n        const fieldInfo = (await this.loadFieldInfo(this.props.resModel, this.state.path)).fieldDef;\n        const filename_exists = (\n            await this.loadFieldInfo(this.props.resModel, this.state.path + \"_filename\")\n        ).fieldDef;\n        const is_image = fieldInfo.type == \"binary\" && !filename_exists;\n        this.props.validate(\n            this.state.path,\n            this.state.defaultValue,\n            is_image,\n            fieldInfo.relation,\n            fieldInfo.string\n        );\n        this.props.close();\n    }\n}\n", "/** @odoo-module */\nimport { Component, onWillStart, onWillUnmount, toRaw, useState } from \"@odoo/owl\";\nimport { XmlResourceEditor } from \"@web_studio/client_action/xml_resource_editor/xml_resource_editor\";\nimport { useEditorMenuItem } from \"@web_studio/client_action/editor/edition_flow\";\nimport { ReportEditorSnackbar } from \"@web_studio/client_action/report_editor/report_editor_snackbar\";\nimport { ReportRecordNavigation } from \"./report_record_navigation\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { ReportEditorIframe } from \"../report_editor_iframe\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { TranslationDialog } from \"@web/views/fields/translation_dialog\";\nimport { View } from \"@web/views/view\";\n\nclass ReportResourceEditor extends XmlResourceEditor {\n    static props = { ...XmlResourceEditor.props, slots: Object };\n    setup() {\n        super.setup();\n        useBus(this.env.reportEditorModel.bus, \"node-clicked\", (ev) => {\n            const { viewId } = ev.detail;\n            const nextResource = this.state.resourcesOptions.find((opt) => opt.value === viewId);\n            if (nextResource) {\n                this.state.currentResourceId = nextResource.value;\n            }\n        });\n    }\n}\n\nclass TranslationButton extends Component {\n    static template = \"web.TranslationButton\";\n    static props = {\n        resourceId: Number,\n    };\n\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n\n    get isMultiLang() {\n        return localization.multiLang;\n    }\n    get lang() {\n        return new Intl.Locale(user.lang).language.toUpperCase();\n    }\n    onClick() {\n        this.addDialog(TranslationDialog, {\n            fieldName: \"arch_db\",\n            resModel: \"ir.ui.view\",\n            resId: this.props.resourceId,\n            onSave: () => {\n                const model = this.env.reportEditorModel;\n                model.loadReportHtml({ resId: model.reportEnv.currentId });\n            },\n        });\n    }\n}\n\nclass _View extends View {\n    async loadView() {\n        const res = await super.loadView(...arguments);\n        const Controller = this.Controller;\n        if (\n            !(\"afterExecuteActionButton\" in Controller.props) &&\n            \"afterExecuteActionButton\" in Controller.prototype\n        ) {\n            class _Controller extends Controller {\n                afterExecuteActionButton(clickParams) {\n                    const res = super.afterExecuteActionButton(...arguments);\n                    this.props.afterExecuteActionButton(this.model, ...arguments);\n                    return res;\n                }\n            }\n            _Controller.props = {\n                ...Controller.props,\n                afterExecuteActionButton: { type: Function },\n            };\n            this.Controller = _Controller;\n        }\n        return res;\n    }\n}\n\nexport class ReportEditorXml extends Component {\n    static components = {\n        XmlResourceEditor: ReportResourceEditor,\n        ReportRecordNavigation,\n        ReportEditorIframe,\n        TranslationButton,\n        View: _View,\n    };\n    static template = \"web_studio.ReportEditorXml\";\n    static props = {\n        paperFormatStyle: String,\n    };\n\n    setup() {\n        this.reportEditorModel = useState(this.env.reportEditorModel);\n        this.state = useState({\n            xmlChanges: null,\n            reloadSources: 1,\n            viewIdToDiff: false,\n            warningMessage: \"\",\n            get isDirty() {\n                return !!this.xmlChanges;\n            },\n        });\n\n        useEditorMenuItem({\n            component: ReportEditorSnackbar,\n            props: {\n                state: this.state,\n                onSave: this.save.bind(this),\n                onDiscard: this.discardChanges.bind(this),\n            },\n        });\n\n        onWillStart(() => this.reportEditorModel.loadReportHtml());\n\n        onWillUnmount(() => {\n            this.save({ urgent: true });\n        });\n    }\n\n    get minWidth() {\n        const factor = this.state.viewIdToDiff ? 0.2 : 0.4;\n        return Math.floor(document.documentElement.clientWidth * factor);\n    }\n\n    async onCloseXmlEditor() {\n        await this.save();\n        this.reportEditorModel.mode = \"wysiwyg\";\n    }\n\n    onXmlChanged(changes) {\n        this.state.xmlChanges = changes;\n    }\n\n    getDefaultResource(resourcesOptions, mainKey) {\n        let mainResource;\n        if (mainKey) {\n            mainResource = resourcesOptions.find(opt => opt.resource.key === mainKey);\n        }\n        if (mainResource) {\n            const studioExtension = resourcesOptions.find(opt => {\n                const key = opt.resource.key;\n                const parentId = opt.resource.inherit_id && opt.resource.inherit_id[0];\n                return key.includes(\"web_studio.report_editor_customization\") &&\n                    parentId === mainResource.resource.id;\n            })\n            return studioExtension || mainResource;\n        }\n    }\n\n    async save({ urgent = false } = {}) {\n        const changes = { ...toRaw(this.state.xmlChanges) };\n        const result = await this.reportEditorModel.saveReport({\n            urgent,\n            xmlVerbatim: changes,\n        });\n        this.state.warningMessage = this.reportEditorModel.warningMessage;\n        if (result !== false) {\n            this.state.xmlChanges = null;\n            if (!urgent && Object.keys(changes).length) {\n                this.state.reloadSources++;\n            }\n        }\n    }\n\n    async discardChanges() {\n        this.state.xmlChanges = null;\n        this.state.reloadSources++;\n    }\n\n    onIframeLoaded({ iframeRef }) {\n        iframeRef.el.contentWindow.document.addEventListener(\"click\", (ev) => {\n            const target = ev.target;\n            const brandingTarget = target.closest(\n                `[data-oe-model=\"ir.ui.view\"][data-oe-field=\"arch\"]`\n            );\n            if (!brandingTarget) {\n                return;\n            }\n            const viewId = parseInt(brandingTarget.getAttribute(\"data-oe-id\"));\n            this.reportEditorModel.bus.trigger(\"node-clicked\", { viewId });\n        });\n        this.reportEditorModel.setInEdition(false);\n    }\n\n    async switchToDiff(viewId) {\n        await this.save();\n        this.state.viewIdToDiff = viewId;\n    }\n\n    get diffProps() {\n        return {\n            type: \"form\",\n            resModel: \"reset.view.arch.wizard\",\n            context: {\n                studio: false,\n                studio_report_diff: true,\n                active_ids: [this.state.viewIdToDiff],\n                active_model: \"ir.ui.view\",\n            },\n            afterExecuteActionButton: (model, clickParams) => {\n                if (\n                    model.root.resModel === \"reset.view.arch.wizard\" &&\n                    clickParams.name === \"reset_view_button\"\n                ) {\n                    this.state.reloadSources++;\n                    this.reportEditorModel._resetInternalArchs();\n                    this.reportEditorModel.loadReportHtml();\n                }\n            },\n            preventCreate: true,\n        };\n    }\n\n    onResourceChanged(resource) {\n        if (this.state.viewIdToDiff) {\n            this.state.viewIdToDiff = resource.id;\n        }\n    }\n}\n", "/** @odoo-module */\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { Pager } from \"@web/core/pager/pager\";\nimport { RecordSelector } from \"@web/core/record_selectors/record_selector\";\n\nexport class ReportRecordNavigation extends Component {\n    static components = { RecordSelector, Pager };\n    static template = \"web_studio.ReportEditor.ReportRecordNavigation\";\n    static props = {};\n\n    setup() {\n        this.reportEditorModel = useState(this.env.reportEditorModel);\n    }\n\n    get multiRecordSelectorProps() {\n        const currentId = this.reportEditorModel.reportEnv.currentId;\n        return {\n            resModel: this.reportEditorModel.reportResModel,\n            update: (resId) => {\n                this.reportEditorModel.loadReportHtml({ resId });\n            },\n            resId: currentId,\n            domain: this.reportEditorModel.getModelDomain(),\n            context: { studio: false },\n        };\n    }\n\n    get pagerProps() {\n        const { reportEnv } = this.reportEditorModel;\n        const { ids, currentId } = reportEnv;\n        return {\n            limit: 1,\n            offset: ids.indexOf(currentId),\n            total: ids.length,\n        };\n    }\n\n    updatePager({ offset }) {\n        const ids = this.reportEditorModel.reportEnv.ids;\n        const resId = ids[offset];\n        this.reportEditorModel.loadReportHtml({ resId });\n    }\n}\n", "/** @odoo-module */\n\nconst PAPER_TO_CSS = {\n    margin_top: \"padding-top\",\n    margin_left: \"padding-left\",\n    margin_right: \"padding-right\",\n    print_page_width: \"width\",\n    print_page_height: \"min-height\",\n};\n\nexport function getCssFromPaperFormat(paperFormat, unit = \"mm\") {\n    return Object.entries(paperFormat)\n        .map((f) => `${PAPER_TO_CSS[f[0]]}:${f[1]}${unit}`)\n        .join(\";\");\n}\n\nconst RECORDSET_RE = /(?<resModel>(\\w+.?)*)\\((?<resIds>(\\d*,?)*)\\)/;\nfunction recordSetReprToData(string) {\n    const { resModel, resIds } = string.match(RECORDSET_RE).groups;\n    return {\n        resModel,\n        resIds: resIds.split(\",\").flatMap((id) => (id ? parseInt(id) : [])),\n    };\n}\n\nexport function humanReadableError(error) {\n    if (error.code === 200 && error.data) {\n        error = error.data;\n    }\n    let viewError;\n    if (error.context?.view) {\n        // see @ def _raise_view_error.\n        const { resIds, resModel } = recordSetReprToData(error.context.view);\n        const { resIds: parentIds } = recordSetReprToData(error.context[\"view.parent\"]);\n        const viewName = error.context.name;\n        viewError = {\n            viewModel: resModel,\n            completeName: error.context.xml_id ? `${viewName} (${error.context.xml_id})` : viewName,\n            resIds,\n            resModel: error.context[\"view.model\"],\n            parentIds,\n        };\n    }\n    return {\n        ...error,\n        traceback: error.debug,\n        viewError,\n    };\n}\n\nexport function visitNode(el, callback) {\n    const iterators = [[el]];\n    while (iterators.length) {\n        const it = iterators.pop();\n        for (const _el of it) {\n            const doChildren = callback(_el);\n            if (doChildren === false) {\n                continue;\n            }\n            iterators.push(_el.children);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { user as originalUser } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { computeAppsAndMenuItems, reorderApps } from \"@web/webclient/menus/menu_helpers\";\n\nimport { AppCreator } from \"./app_creator/app_creator\";\nimport { Editor } from \"./editor/editor\";\nimport { StudioNavbar } from \"./navbar/navbar\";\nimport { StudioHomeMenu } from \"./studio_home_menu/studio_home_menu\";\n\nimport { Component, onWillStart, onMounted, onPatched, onWillUnmount } from \"@odoo/owl\";\n\nexport class StudioClientAction extends Component {\n    static template = \"web_studio.StudioClientAction\";\n    static target = \"fullscreen\";\n    static props = { ...standardActionServiceProps };\n    static components = {\n        StudioNavbar,\n        StudioHomeMenu,\n        Editor,\n        AppCreator,\n    };\n\n    setup() {\n        const homemenuConfig = JSON.parse(originalUser.settings?.homemenu_config || \"null\");\n        this.studio = useService(\"studio\");\n        useBus(this.studio.bus, \"UPDATE\", () => {\n            this.render();\n        });\n\n        this.menus = useService(\"menu\");\n        this.actionService = useService(\"action\");\n        let apps = computeAppsAndMenuItems(this.menus.getMenuAsTree(\"root\")).apps;\n        if (homemenuConfig) {\n            reorderApps(apps, homemenuConfig);\n        }\n        this.homeMenuProps = {\n            apps: apps,\n        };\n        useBus(this.env.bus, \"MENUS:APP-CHANGED\", () => {\n            apps = computeAppsAndMenuItems(this.menus.getMenuAsTree(\"root\")).apps;\n            if (homemenuConfig) {\n                reorderApps(apps, homemenuConfig);\n            }\n            this.homeMenuProps = {\n                apps: apps,\n            };\n            this.render();\n        });\n\n        onWillStart(this.onWillStart);\n        onMounted(this.onMounted);\n        onPatched(this.onPatched);\n        onWillUnmount(this.onWillUnmount);\n    }\n\n    onWillStart() {\n        return this.studio.ready;\n    }\n\n    onMounted() {\n        this.studio.pushState();\n        document.body.classList.add(\"o_in_studio\"); // FIXME ?\n    }\n\n    onPatched() {\n        this.studio.pushState();\n    }\n\n    onWillUnmount() {\n        document.body.classList.remove(\"o_in_studio\");\n    }\n\n    async onNewAppCreated({ action_id, menu_id }) {\n        await this.menus.reload();\n        this.menus.setCurrentMenu(menu_id);\n        const action = await this.actionService.loadAction(action_id);\n\n        let initViewType = \"form\";\n        if (!action.views.some((vTuple) => vTuple[1] === initViewType)) {\n            initViewType = action.views[0][1];\n        }\n\n        this.studio.setParams({\n            mode: this.studio.MODES.EDITOR,\n            editorTab: \"views\",\n            action,\n            viewType: initViewType,\n        });\n    }\n}\n\nregistry.category(\"lazy_components\").add(\"StudioClientAction\", StudioClientAction);\n// force: true to bypass the studio lazy loading action next time and just use this one directly\nregistry.category(\"actions\").add(\"studio\", StudioClientAction, { force: true });\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { IconCreator } from \"@web_studio/client_action/icon_creator/icon_creator\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class IconCreatorDialog extends Component {\n    static props = {\n        editedAppData: Object,\n        appId: Number,\n        close: Function,\n    };\n    static template = \"web_studio.IconCreatorDialog\";\n    static components = { Dialog, IconCreator };\n\n    setup() {\n        this.menus = useService(\"menu\");\n        this.initialAppData = { ...this.props.editedAppData };\n        this.editedAppData = useState(this.props.editedAppData);\n    }\n\n    /**\n     * @param {Object} icon\n     */\n    onIconChanged(icon) {\n        for (const key in this.editedAppData) {\n            delete this.editedAppData[key];\n        }\n        for (const key in icon) {\n            this.editedAppData[key] = icon[key];\n        }\n    }\n\n    async saveIcon() {\n        const { type } = this.initialAppData;\n        const appId = this.props.appId;\n        let iconValue;\n        if (this.editedAppData.type !== type) {\n            // different type\n            if (this.editedAppData.type === \"base64\") {\n                iconValue = this.editedAppData.uploaded_attachment_id;\n            } else {\n                const { iconClass, color, backgroundColor } = this.editedAppData;\n                iconValue = [iconClass, color, backgroundColor];\n            }\n        } else if (this.editedAppData.type === \"custom_icon\") {\n            // custom icon changed\n            const { iconClass, color, backgroundColor } = this.editedAppData;\n            if (\n                this.initialAppData.iconClass !== iconClass ||\n                this.initialAppData.color !== color ||\n                this.initialAppData.backgroundColor !== backgroundColor\n            ) {\n                iconValue = [iconClass, color, backgroundColor];\n            }\n        } else if (this.editedAppData.uploaded_attachment_id) {\n            // new attachment\n            iconValue = this.editedAppData.uploaded_attachment_id;\n        }\n\n        if (iconValue) {\n            await rpc(\"/web_studio/edit_menu_icon\", {\n                context: user.context,\n                icon: iconValue,\n                menu_id: appId,\n            });\n            await this.menus.reload();\n        }\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { HomeMenu } from \"@web_enterprise/webclient/home_menu/home_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { NotEditableActionError } from \"../../studio_service\";\nimport { IconCreatorDialog } from \"./icon_creator_dialog/icon_creator_dialog\";\n\nimport { onMounted, onWillUnmount, useRef } from \"@odoo/owl\";\nconst NEW_APP_BUTTON = {\n    isNewAppButton: true,\n    label: _t(\"New App\"),\n    webIconData: \"/web_studio/static/src/img/default_icon_app.png\",\n};\n\n/**\n * Studio home menu\n *\n * Studio version of the standard enterprise home menu. It has roughly the same\n * implementation, with the exception of the app icon edition and the app creator.\n * @extends HomeMenu\n */\nexport class StudioHomeMenu extends HomeMenu {\n    static props = { apps: HomeMenu.props.apps };\n    static template = \"web_studio.StudioHomeMenu\";\n\n    /**\n     * @param {Object} props\n     * @param {Object[]} props.apps application icons\n     * @param {string} props.apps[].action\n     * @param {number} props.apps[].id\n     * @param {string} props.apps[].label\n     * @param {string} props.apps[].parents\n     * @param {(boolean|string|Object)} props.apps[].webIcon either:\n     *      - boolean: false (no webIcon)\n     *      - string: path to Odoo icon file\n     *      - Object: customized icon (background, class and color)\n     * @param {string} [props.apps[].webIconData]\n     * @param {string} props.apps[].xmlid\n     */\n    setup() {\n        super.setup(...arguments);\n\n        this.studio = useService(\"studio\");\n        this.notifications = useService(\"notification\");\n        this.dialog = useService(\"dialog\");\n        this.root = useRef(\"root\");\n\n        onMounted(() => {\n            this.canEditIcons = true;\n            document.body.classList.add(\"o_home_menu_background\");\n            document.body.classList.toggle(\n                \"o_home_menu_background_custom\",\n                this.menus.getMenu(\"root\").backgroundImage\n            );\n        });\n\n        onWillUnmount(() => {\n            document.body.classList.remove(\n                \"o_home_menu_background\",\n                \"o_home_menu_background_custom\"\n            );\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    get displayedApps() {\n        return [...super.displayedApps, NEW_APP_BUTTON];\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    async _openMenu(menu) {\n        if (menu.isNewAppButton) {\n            this.canEditIcons = false;\n            return this.studio.open(this.studio.MODES.APP_CREATOR);\n        } else {\n            try {\n                await this.studio.open(this.studio.MODES.EDITOR, menu.actionID);\n                this.menus.setCurrentMenu(menu);\n            } catch (e) {\n                if (e instanceof NotEditableActionError) {\n                    const options = { type: \"danger\" };\n                    this.notifications.add(_t(\"This action is not editable by Studio\"), options);\n                    return;\n                }\n                throw e;\n            }\n        }\n    }\n\n    _enableAppsSorting() {\n        return false;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {Object} app\n     */\n    onEditIconClick(app) {\n        if (!this.canEditIcons) {\n            return;\n        }\n        const editedAppData = {};\n        if (app.webIconData) {\n            Object.assign(editedAppData, {\n                webIconData: app.webIconData,\n                type: \"base64\",\n            });\n        } else {\n            Object.assign(editedAppData, {\n                backgroundColor: app.webIcon.backgroundColor,\n                color: app.webIcon.color,\n                iconClass: app.webIcon.iconClass,\n                type: \"custom_icon\",\n            });\n        }\n\n        const dialogProps = {\n            editedAppData,\n            appId: app.id,\n        };\n        this.dialog.add(IconCreatorDialog, dialogProps);\n    }\n}\n", "/** @odoo-module */\nimport { reactive, useComponent, useEnv, useSubEnv } from \"@odoo/owl\";\n\nexport function getFieldsInArch(xmlDoc) {\n    const res = [];\n    const isInvisible = [\"True\", \"1\", \"true\"];\n    xmlDoc.querySelectorAll(\"field\").forEach((el) => {\n        if (!el.parentElement.closest(\"field,groupby\")) {\n            const invisible = el.getAttribute(\"invisible\") || el.getAttribute(\"column_invisible\");\n            const dataUsedBy = el.getAttribute(\"data-used-by\");\n            if (dataUsedBy) {\n                return;\n            }\n            if (!invisible || !isInvisible.includes(invisible)) {\n                res.push(el.getAttribute(\"name\"));\n            }\n        }\n    });\n    return res;\n}\n\nexport function useDialogConfirmation({ confirm, cancel, before, close }) {\n    before = before || (() => {});\n    confirm = confirm || (() => {});\n    cancel = cancel || (() => {});\n    if (!close) {\n        const component = useComponent();\n        close = () => component.props.close();\n    }\n\n    let isProtected = false;\n    async function canExecute() {\n        if (isProtected) {\n            return false;\n        }\n        isProtected = true;\n        await before();\n        return true;\n    }\n\n    async function execute(cb, ...args) {\n        let succeeded = false;\n        try {\n            succeeded = await cb(...args);\n        } catch (e) {\n            close();\n            throw e;\n        }\n        if (succeeded === undefined || succeeded) {\n            return close();\n        }\n        isProtected = false;\n    }\n\n    async function _confirm(...args) {\n        if (!(await canExecute())) {\n            return;\n        }\n        return execute(confirm, ...args);\n    }\n\n    async function _cancel(...args) {\n        if (!(await canExecute())) {\n            return;\n        }\n        return execute(cancel, ...args);\n    }\n\n    const env = useEnv();\n    env.dialogData.dismiss = () => _cancel();\n\n    return { confirm: _confirm, cancel: _cancel };\n}\n\nexport class Reactive {\n    constructor() {\n        const raw = this;\n        // A function not bound to this returning the original not reactive object\n        // This is usefull to be able to read stuff without subscribing the caller\n        // eg: when reading internals just for checking\n        this.raw = () => {\n            return raw;\n        };\n        return reactive(this);\n    }\n}\n\n// A custom memoize function that doesn't store all results\n// First the core/function/memoize tool may yield incorrect result in our case.\n// Second, the keys we use usually involve archs themselves that could be heavy in the long run.\nexport function memoizeOnce(callback) {\n    let key, value;\n    return function (...args) {\n        if (key === args[0]) {\n            return value;\n        }\n        key = args[0];\n        value = callback.call(this, ...args);\n        return value;\n    };\n}\n\nexport function useSubEnvAndServices(env) {\n    const services = env.services;\n    const bus = env.bus;\n    useSubEnv(env);\n    useSubEnv({ services, bus });\n}\n\n/**\n * Sorts a list topologically, each element's dependencies should be defined\n * with the getDependencies callback.\n * This is a copy of what is done in python: odoo.tools.misc.py:def topological_sort\n * @params [Array] elems\n * @params [Function] getDependencies\n */\nfunction topologicalSort(elems, getDependencies) {\n    const result = [];\n    const visited = new Set();\n    function visit(n) {\n        if (visited.has(n)) {\n            return;\n        }\n        visited.add(n);\n        if (!elems.includes(n)) {\n            return;\n        }\n        // first visit all dependencies of n, then append n to result\n        for (const dep of getDependencies(n)) {\n            visit(dep);\n        }\n        result.push(n);\n    }\n\n    for (const el of elems) {\n        visit(el);\n    }\n\n    return result;\n}\n\n/**\n * Allows to override the services defined in the env with a new instance\n * of each one defined in \"overrides\".\n * This function assumes all services in overrides start synchronously\n * @params [Object] overrides: new instances of services to create\n *     the key is the service's name, the value is the service definition\n */\nexport function useServicesOverrides(overrides) {\n    let env = useEnv();\n    const services = Object.create(env.services);\n\n    useSubEnv({ services });\n    env = useEnv();\n    const getDependencies = (name) => overrides[name]?.dependencies || [];\n    const topoSorted = topologicalSort(Object.keys(overrides), getDependencies);\n\n    for (const servName of topoSorted) {\n        services[servName] = overrides[servName].start(env, services);\n    }\n}\n", "/** @odoo-module */\nimport { Component, useState } from \"@odoo/owl\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\n\nexport class DefaultViewSidebar extends Component {\n    static template = \"web_studio.ViewEditor.DefaultViewSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n    }\n}\n", "/** @odoo-module */\n\nimport { calendarView } from \"@web/views/calendar/calendar_view\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { SCALE_LABELS } from \"@web/views/calendar/calendar_controller\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class CalendarEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.CalendarEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        Property,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n    }\n\n    get archInfo() {\n        return this.viewEditorModel.controllerProps.archInfo;\n    }\n\n    onViewAttributeChanged(value, name) {\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    get quickCreateFields() {\n        return fieldsToChoices(this.viewEditorModel.fields, [\"char\"], (field) => field.store);\n    }\n\n    get startDateFields() {\n        return fieldsToChoices(this.viewEditorModel.fields, [\"date\", \"datetime\"]);\n    }\n\n    get delayFields() {\n        return fieldsToChoices(this.viewEditorModel.fields, [\"float\", \"integer\"]);\n    }\n\n    get colorFields() {\n        return fieldsToChoices(this.viewEditorModel.fields, [\"many2one\", \"selection\", \"integer\"]);\n    }\n\n    get allDayFields() {\n        return fieldsToChoices(this.viewEditorModel.fields, [\"boolean\"]);\n    }\n\n    get modeChoices() {\n        return this.viewEditorModel.controllerProps.archInfo.scales.map((value) => {\n            return {\n                value,\n                label: SCALE_LABELS[value],\n            };\n        });\n    }\n}\n\nregistry.category(\"studio_editors\").add(\"calendar\", {\n    ...calendarView,\n    Sidebar: CalendarEditorSidebar,\n});\n", "/** @odoo-module */\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { cohortView } from \"@web_cohort/cohort_view\";\n\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class CohortEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.CohortEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        Property,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n    }\n\n    onViewAttributeChanged(value, name) {\n        value = value ? value : \"\";\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    get modelParams() {\n        return this.env.viewEditorModel.controllerProps.modelParams;\n    }\n\n    get dateFields() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            [\"date\", \"datetime\"],\n            (field) => field.store\n        );\n    }\n\n    get measureFields() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            [\"integer\", \"float\", \"monetary\"],\n            (field) => field.name !== \"id\" && field.store\n        );\n    }\n\n    get intervalChoices() {\n        return [\n            { label: _t(\"Day\"), value: \"day\" },\n            { label: _t(\"Week\"), value: \"week\" },\n            { label: _t(\"Month\"), value: \"month\" },\n            { label: _t(\"Year\"), value: \"year\" },\n        ];\n    }\n\n    get modeChoices() {\n        return [\n            { label: _t(\"Retention\"), value: \"retention\" },\n            { label: _t(\"Churn\"), value: \"churn\" },\n        ];\n    }\n\n    get timelineChoices() {\n        return [\n            { label: _t(\"Forward\"), value: \"forward\" },\n            { label: _t(\"Backwards\"), value: \"backward\" },\n        ];\n    }\n}\n\nregistry.category(\"studio_editors\").add(\"cohort\", {\n    ...cohortView,\n    Sidebar: CohortEditorSidebar,\n});\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, xml } from \"@odoo/owl\";\nimport { viewTypeToString } from \"@web_studio/studio_service\";\n\n/*\n * Injected in the Field.js template\n * Allows to overlay the Field's Component widget to prompt\n * for editing a x2many subview\n */\nexport class FieldContentOverlay extends Component {\n    static template = xml`\n    <div class=\"position-relative\">\n      <t t-slot=\"default\" />\n      <div class=\"o-web-studio-edit-x2manys-buttons w-100 h-100 d-flex justify-content-center gap-3 position-absolute start-0 top-0 opacity-75 bg-dark\" t-if=\"props.displayOverlay\" style=\"z-index: 1000;\">\n          <button class=\"btn btn-primary btn-secondary o_web_studio_editX2Many align-self-center\"\n          t-foreach=\"['list', 'form']\" t-as=\"vType\" t-key=\"vType\"\n          t-on-click.stop=\"() => props.onEditViewType(vType)\"\n          t-att-data-type=\"vType\">\n          <t t-esc=\"getButtonText(vType)\" />\n          </button>\n      </div>\n    </div>`;\n\n    static props = {\n        displayOverlay: { type: Boolean },\n        slots: { type: Object },\n        onEditViewType: { type: Function },\n    };\n\n    getButtonText(viewType) {\n        return _t(\"Edit %s view\", viewTypeToString(viewType));\n    }\n}\n", "/** @odoo-module */\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class FieldSelectorDialog extends Component {\n    static template = \"web_studio.FieldSelectorDialog\";\n    static components = { Dialog };\n    static props = {\n        close: { type: Function },\n        onConfirm: { type: Function },\n        fields: { type: Array },\n        showNew: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showNew: false,\n    };\n    setup() {\n        this.selectRef = useRef(\"select\");\n    }\n    onConfirm() {\n        const field = this.selectRef.el.value;\n        this.props.onConfirm(field);\n        this.props.close();\n    }\n    onCancel() {\n        this.props.close();\n    }\n}\n", "/** @odoo-module */\nimport { Field } from \"@web/views/fields/field\";\nimport { FieldContentOverlay } from \"./field_content_overlay\";\n\nimport { useStudioRef, studioIsVisible } from \"@web_studio/client_action/view_editor/editors/utils\";\n\nimport { useState } from \"@odoo/owl\";\n\n/*\n * Field:\n * - Displays an Overlay for X2Many fields\n * - handles invisible\n */\nexport class FieldStudio extends Field {\n    static components = { ...Field.components, FieldContentOverlay };\n    static template = \"web_studio.Field\";\n    setup() {\n        super.setup();\n        this.state = useState({\n            displayOverlay: false,\n        });\n        useStudioRef(\"rootRef\", this.onClick);\n    }\n    get fieldComponentProps() {\n        const fieldComponentProps = super.fieldComponentProps;\n        if (this.type === \"kanban.many2one_avatar_user\") {\n            // This field must be visible, even when dealing without a record\n            fieldComponentProps.isEditable = true;\n        }\n        delete fieldComponentProps.studioXpath;\n        delete fieldComponentProps.hasEmptyPlaceholder;\n        delete fieldComponentProps.hasLabel;\n        delete fieldComponentProps.studioIsVisible;\n        return fieldComponentProps;\n    }\n    get classNames() {\n        const classNames = super.classNames;\n        classNames[\"o_web_studio_show_invisible\"] = !studioIsVisible(this.props);\n        classNames[\"o-web-studio-editor--element-clickable\"] = !!this.props.studioXpath;\n        if (this.studioIsEmpty()) {\n            delete classNames[\"o_field_empty\"];\n            classNames[\"o_web_studio_widget_empty\"] = true;\n            classNames[\"text-muted\"] = true;\n        }\n        return classNames;\n    }\n\n    studioIsEmpty() {\n        const { name, record, hasLabel } = this.props;\n        if (hasLabel) {\n            return false;\n        }\n        if (this.type === \"kanban.many2one_avatar_user\") {\n            return false; // The widget has its own visibility when empty\n        }\n        return \"isEmpty\" in this.field ? this.field.isEmpty(record, name) : !record.data[name];\n    }\n\n    getEmptyPlaceholder() {\n        const { hasEmptyPlaceholder, name, record, fieldInfo } = this.props;\n        if (!hasEmptyPlaceholder) {\n            return false;\n        }\n        return this.studioIsEmpty() && (fieldInfo.string || record.fields[name].string);\n    }\n\n    isX2ManyEditable(props) {\n        const { name, record } = props;\n        const field = record.fields[name];\n        if (![\"one2many\", \"many2many\"].includes(field.type)) {\n            return false;\n        }\n        return !!this.props.fieldInfo.field.useSubView;\n    }\n\n    onEditViewType(viewType) {\n        const { name, record, studioXpath } = this.props;\n        this.env.viewEditorModel.editX2ManyView({\n            viewType,\n            fieldName: name,\n            record,\n            xpath: studioXpath,\n            fieldContext: this.fieldComponentProps.context,\n        });\n    }\n\n    onClick(ev) {\n        if (ev.target.classList.contains(\"o_web_studio_editX2Many\")) {\n            return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.env.config.onNodeClicked(this.props.studioXpath);\n        this.state.displayOverlay = !this.state.displayOverlay;\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\n\nconst formGrid = xml`\n    <div class=\"o_web_studio_hook\"\n        t-attf-class=\"g-col-sm-{{ props.colSpan }}\"\n        t-att-data-xpath=\"props.xpath\"\n        t-att-data-position=\"props.position\"\n        t-att-data-type=\"props.type\">\n            <span class=\"o_web_studio_hook_separator\" />\n    </div>\n`;\n\nconst kanbanAsideHook = xml`\n    <div t-attf-class=\"o_web_studio_hook mx-1 o_web_studio_hook position-absolute top-0 h-100 pe-none w-0 {{ props.position === 'before' ? 'start-0' : 'end-0'}}\" t-att-data-type=\"props.type\" t-att-data-xpath=\"props.xpath\" t-att-data-position=\"props.position\" data-structures=\"aside\" />\n`;\n\nconst kanbanRibbon = xml`\n    <div class=\"o_web_studio_hook position-absolute top-0 start-0 h-100 overflow-hidden m-0 p-0 pe-none w-0\" t-att-data-type=\"props.type\" t-att-data-xpath=\"props.xpath\" data-position=\"inside\" data-structures=\"ribbon\">\n        <div class=\"bg-primary opacity-0 position-absolute\" style=\"transform:rotate(45deg); height: 25px; width: 140px; top: 10px; right: -30px;\" />\n    </div>\n`;\n\nconst kanbanInline = xml`\n    <span class=\"o_web_studio_hook\" t-att-data-xpath=\"props.xpath\" t-att-data-position=\"props.position\" t-att-data-type=\"props.type\" t-att-data-infos=\"props.infos\" t-att-data-structures=\"props.structures\" />\n`;\n\nconst defaultTemplate = xml`\n<div class=\"o_web_studio_hook\" t-att-data-xpath=\"props.xpath\" t-att-data-position=\"props.position\" t-att-data-type=\"props.type\" t-att-data-infos=\"props.infos\" t-att-data-structures=\"props.structures\">\n    <span class=\"o_web_studio_hook_separator\" />\n</div>\n`;\n\nexport class StudioHook extends Component {\n    static template = xml`<t t-call=\"{{ getTemplate(props.subTemplate) }}\" />`;\n    static props = [\n        \"xpath?\",\n        \"position?\",\n        \"type?\",\n        \"colSpan?\",\n        \"subTemplate?\",\n        \"width?\",\n        \"infos?\",\n        \"structures?\",\n    ];\n    static subTemplates = {\n        formGrid,\n        defaultTemplate,\n        kanbanInline,\n        kanbanAsideHook,\n        kanbanRibbon,\n    };\n\n    getTemplate(templateName) {\n        return this.constructor.subTemplates[templateName || \"defaultTemplate\"];\n    }\n}\n", "import { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useStudioRef, studioIsVisible } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\n/*\n * ViewButton:\n * - Deals with invisible\n * - Click is overriden not to trigger the bound action\n */\nexport class ViewButtonStudio extends ViewButton {\n    static template = \"web_studio.ViewButton\";\n    static props = [...ViewButton.props, \"studioIsVisible?\", \"studioXpath?\"];\n\n    setup() {\n        super.setup();\n        useStudioRef(\"rootRef\");\n\n        useBus(this.env.viewEditorModel.env.bus, \"approval-update\", () => {\n            this.approval.fetchApprovals();\n        });\n    }\n    getClassName() {\n        let className = super.getClassName();\n        if (!studioIsVisible(this.props)) {\n            className += \" o_web_studio_show_invisible\";\n        }\n        if (this.props.studioXpath) {\n            className += \" o-web-studio-editor--element-clickable\";\n        }\n        return className;\n    }\n\n    onClick(ev) {\n        if (this.props.tag === \"a\") {\n            ev.preventDefault();\n        }\n        const target = ev.target.classList.contains(\"o-web-studio-editor--element-clickable\")\n            ? ev.target\n            : ev.target.closest(\"o-web-studio-editor--element-clickable\");\n        this.env.config.onNodeClicked(\n            target?.getAttribute(\"studioxpath\") || this.props.studioXpath\n        );\n    }\n\n    _shouldUseApproval() {\n        return true;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SidebarDraggableItem } from \"@web_studio/client_action/components/sidebar_draggable_item/sidebar_draggable_item\";\n\nexport class ExistingFields extends Component {\n    static components = { SidebarDraggableItem };\n    static props = {\n        fieldsInArch: { type: Array },\n        fields: { type: Object },\n        filterFields: { type: Boolean, optional: true },\n        folded: { type: Boolean, optional: true },\n        resModel: { type: String, optional: true },\n    };\n    static defaultProps = {\n        folded: true,\n        filterFields: true,\n        resModel: \"\",\n    };\n    static template = \"web_studio.ViewFields.ExistingFields\";\n\n    setup() {\n        this.state = useState({\n            folded: this.props.folded,\n            searchValue: \"\",\n        });\n    }\n\n    isMatchingSearch(field) {\n        if (!this.state.searchValue) {\n            return true;\n        }\n        const search = this.state.searchValue.toLowerCase();\n        let matches = field.string.toLowerCase().includes(search);\n        if (!matches && this.env.debug && field.name) {\n            matches = field.name.toLowerCase().includes(search);\n        }\n        return matches;\n    }\n\n    get existingFields() {\n        const fieldsInArch = this.props.fieldsInArch;\n        const resModel = this.props.resModel;\n        const filtered = Object.entries(this.props.fields).filter(([fName, field]) => {\n            if (\n                resModel === \"res.users\" &&\n                (fName.startsWith(\"in_group_\") || fName.startsWith(\"sel_groups_\"))\n            ) {\n                // These fields are virtual and represent res.groups hierarchy.\n                // If the hierarchy changes, the field is replaced by another one and the view will be\n                // broken, so, here we prevent adding them.\n                return false;\n            }\n            if (\n                !this.isMatchingSearch(field) ||\n                (this.props.filterFields && fieldsInArch.includes(fName))\n            ) {\n                return false;\n            }\n            return true;\n        });\n\n        return filtered.map(([fName, field]) => {\n            return {\n                ...field,\n                name: fName,\n                classType: field.type,\n                dropData: JSON.stringify({ fieldName: fName }),\n            };\n        });\n    }\n\n    getDropInfo(field) {\n        return {\n            structure: \"field\",\n            fieldName: field.name,\n            isNew: false,\n        };\n    }\n}\n\nconst newFields = [\n    { type: \"char\", string: _t(\"Text\") },\n    { type: \"text\", string: _t(\"Multine Text\") },\n    { type: \"integer\", string: _t(\"Integer\") },\n    { type: \"float\", string: _t(\"Decimal\") },\n    { type: \"html\", string: _t(\"HTML\") },\n    { type: \"monetary\", string: _t(\"Monetary\") },\n    { type: \"date\", string: _t(\"Date\") },\n    { type: \"datetime\", string: _t(\"Datetime\") },\n    { type: \"boolean\", string: _t(\"CheckBox\") },\n    { type: \"selection\", string: _t(\"Selection\") },\n    { type: \"binary\", string: _t(\"File\"), widget: \"file\" },\n    { type: \"one2many\", string: _t(\"Lines\"), special: \"lines\" },\n    { type: \"one2many\", string: _t(\"One2Many\") },\n    { type: \"many2one\", string: _t(\"Many2One\") },\n    { type: \"many2many\", string: _t(\"Many2Many\") },\n    { type: \"binary\", string: _t(\"Image\"), widget: \"image\", name: \"picture\" },\n    { type: \"many2many\", string: _t(\"Tags\"), widget: \"many2many_tags\", name: \"tags\" },\n    { type: \"selection\", string: _t(\"Priority\"), widget: \"priority\" },\n    { type: \"binary\", string: _t(\"Signature\"), widget: \"signature\" },\n    { type: \"related\", string: _t(\"Related Field\") },\n];\n\nexport class NewFields extends Component {\n    static components = { SidebarDraggableItem };\n    static props = {};\n    static template = \"web_studio.ViewFields.NewFields\";\n\n    get newFieldsComponents() {\n        return newFields.map((f) => {\n            const classType = f.special || f.name || f.widget || f.type;\n            return {\n                ...f,\n                name: classType,\n                classType,\n                dropData: JSON.stringify({\n                    fieldType: f.type,\n                    widget: f.widget,\n                    name: f.name,\n                    special: f.special,\n                    string: f.string,\n                }),\n            };\n        });\n    }\n}\n", "import { SidebarDraggableItem } from \"@web_studio/client_action/components/sidebar_draggable_item/sidebar_draggable_item\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ViewStructures extends Component {\n    static components = { SidebarDraggableItem };\n    static template = \"web_studio.ViewStructures\";\n    static props = {\n        structures: { type: Object },\n    };\n    get isVisible() {\n        return Object.values(this.props.structures).filter(\n            (e) => !e.isVisible || e.isVisible(this.env.viewEditorModel)\n        ).length;\n    }\n}\n", "import { Widget } from \"@web/views/widgets/widget\";\nimport { studioIsVisible, useStudioRef } from \"@web_studio/client_action/view_editor/editors/utils\";\n\nexport class WidgetStudio extends Widget {\n    static template = \"web_studio.Widget\";\n    setup() {\n        super.setup();\n        useStudioRef(\"rootRef\", this.onClick);\n    }\n    get classNames() {\n        const classNames = super.classNames;\n        classNames[\"o_web_studio_show_invisible\"] = !studioIsVisible(this.props);\n        classNames[\"o-web-studio-editor--element-clickable\"] = !!this.props.studioXpath;\n        return classNames;\n    }\n    get widgetProps() {\n        const widgetProps = super.widgetProps;\n        delete widgetProps.studioXpath;\n        delete widgetProps.hasEmptyPlaceholder;\n        delete widgetProps.hasLabel;\n        delete widgetProps.studioIsVisible;\n        return widgetProps;\n    }\n    onClick(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.env.config.onNodeClicked(this.props.studioXpath);\n    }\n}\n", "/** @odoo-module */\n\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ChatterContainer extends Chatter {\n    static template = \"web_studio.ChatterContainer\";\n    static props = [...Chatter.props, \"studioXpath?\"];\n\n    onClick(ev) {\n        this.env.config.onNodeClicked(this.props.studioXpath);\n    }\n}\n\nexport class ChatterContainerHook extends Component {\n    static template = \"web_studio.ChatterContainerHook\";\n    static components = { Chatter };\n    static props = {\n        chatterData: Object,\n        threadModel: String,\n    };\n\n    onClick() {\n        this.env.viewEditorModel.doOperation({\n            type: \"chatter\",\n            model: this.env.viewEditorModel.resModel,\n            ...this.props.chatterData,\n        });\n    }\n}\n", "import { formView } from \"@web/views/form/form_view\";\nimport { FormEditorRenderer } from \"./form_editor_renderer/form_editor_renderer\";\nimport { FormEditorController } from \"./form_editor_controller/form_editor_controller\";\nimport { FormEditorCompiler } from \"./form_editor_compiler\";\nimport { registry } from \"@web/core/registry\";\nimport { omit } from \"@web/core/utils/objects\";\nimport {\n    makeModelErrorResilient,\n    randomName,\n} from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { getModifier } from \"@web/views/view_compiler\";\nimport { FormEditorSidebar } from \"./form_editor_sidebar/form_editor_sidebar\";\nimport { getStudioNoFetchFields } from \"../utils\";\n\nclass EditorArchParser extends formView.ArchParser {\n    parse() {\n        const archInfo = super.parse(...arguments);\n        this.omitStudioNoFetchFields(archInfo);\n        return archInfo;\n    }\n\n    omitStudioNoFetchFields(archInfo) {\n        const noFetch = getStudioNoFetchFields(archInfo.fieldNodes);\n        archInfo.fieldNodes = omit(archInfo.fieldNodes, ...noFetch.fieldNodes);\n\n        for (const fieldNode of Object.values(archInfo.fieldNodes)) {\n            if (fieldNode.views) {\n                for (const fieldArchInfo of Object.values(fieldNode.views)) {\n                    this.omitStudioNoFetchFields(fieldArchInfo);\n                }\n            }\n        }\n    }\n}\n\nclass Model extends formView.Model {}\nModel.Record = class RecordNoEdit extends formView.Model.Record {\n    get isInEdition() {\n        return false;\n    }\n    _save() {\n        return true\n    }\n};\n\nexport const formEditor = {\n    ...formView,\n    ArchParser: EditorArchParser,\n    Compiler: FormEditorCompiler,\n    Renderer: FormEditorRenderer,\n    Controller: FormEditorController,\n    props(genericProps, editor, config) {\n        const arch = genericProps.arch;\n        Array.from(arch.querySelectorAll(\"field > list, field > form, field > kanban\")).forEach(\n            (el) => {\n                // Inline subviews sometimes have a \"groups\" attribute, allowing to have different\n                // x2many views depending on access rights. Outside Studio, this has no impact\n                // client side, because the view processing in python would remove nodes with groups\n                // the user doesn't belong to. However, when there's a \"studio\" key in the context,\n                // nodes are no longer removed but they are set as invisible=\"1\" instead. This means\n                // that in Studio, we can have several x2many subviews for the same view type (even\n                // tough, only one of them should be visible). Here, we're only interested in the\n                // views that are visible (the ones the user has access to), so we remove the others.\n                if (getModifier(el, \"invisible\")) {\n                    el.remove();\n                }\n            }\n        );\n        const props = formView.props(genericProps, editor, config);\n        props.Model = makeModelErrorResilient(Model);\n        props.preventEdit = true;\n        return props;\n    },\n    Sidebar: FormEditorSidebar,\n};\nregistry.category(\"studio_editors\").add(\"form\", formEditor);\n\n/**\n *  Drag/Drop Validation\n */\nconst HOOK_CLASS_WHITELIST = [\n    \"o_web_studio_field_signature\",\n    \"o_web_studio_field_html\",\n    \"o_web_studio_field_many2many\",\n    \"o_web_studio_field_one2many\",\n    \"o_web_studio_field_tabs\",\n    \"o_web_studio_field_columns\",\n    \"o_web_studio_field_lines\",\n];\nconst HOOK_TYPE_BLACKLIST = [\"genericTag\", \"afterGroup\", \"afterNotebook\", \"insideSheet\"];\n\nconst isBlackListedHook = (draggedEl, hookEl) =>\n    !HOOK_CLASS_WHITELIST.some((cls) => draggedEl.classList.contains(cls)) &&\n    HOOK_TYPE_BLACKLIST.some((t) => hookEl.dataset.type === t);\n\nfunction canDropNotebook(hookEl) {\n    if (hookEl.dataset.type === \"page\") {\n        return false;\n    }\n    if (hookEl.closest(\".o_group\") || hookEl.closest(\".o_inner_group\")) {\n        return false;\n    }\n    return true;\n}\n\nfunction canDropGroup(hookEl) {\n    if (hookEl.dataset.type === \"insideGroup\") {\n        return false;\n    }\n    if (hookEl.closest(\".o_group\") || hookEl.closest(\".o_inner_group\")) {\n        return false;\n    }\n    return true;\n}\n\nfunction isValidFormHook({ hook, element }) {\n    const draggingStructure = element.dataset.structure;\n    switch (draggingStructure) {\n        case \"notebook\": {\n            if (!canDropNotebook(hook)) {\n                return false;\n            }\n            break;\n        }\n        case \"group\": {\n            if (!canDropGroup(hook)) {\n                return false;\n            }\n            break;\n        }\n    }\n    if (isBlackListedHook(element, hook)) {\n        return false;\n    }\n\n    return true;\n}\nformEditor.isValidHook = isValidFormHook;\n\nfunction addFormViewStructure(structure) {\n    switch (structure) {\n        case \"notebook\":\n        case \"group\": {\n            return {\n                node: {\n                    tag: structure,\n                    attrs: {\n                        name: randomName(`studio_${structure}`),\n                    },\n                },\n            };\n        }\n    }\n}\nformEditor.addViewStructure = addFormViewStructure;\n", "/** @odoo-module */\n\nimport { isComponentNode } from \"@web/views/view_compiler\";\nimport {\n    computeXpath,\n    applyInvisible,\n} from \"@web_studio/client_action/view_editor/editors/xml_utils\";\nimport { createElement } from \"@web/core/utils/xml\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { objectToString } from \"@web/views/form/form_compiler\";\n\nconst interestingSelector = [\n    \":not(field) sheet\", // A hook should be present to add elements in the sheet\n    \":not(field) field\", // should be clickable and draggable\n    \":not(field) notebook\", // should be able to add pages\n    \":not(field) page\", // should be clickable\n    \":not(field) button\", // should be clickable\n    \":not(field) label\", // should be clickable\n    \":not(field) group\", // any group: outer or inner\n    \":not(field) group:not(:has(> group)) > *\", // content of inner groups serves as main dropzone\n    \":not(field) chatter\",\n    \":not(field) .oe_avatar\",\n    \":not(field) widget\", // should be clickable\n].join(\", \");\n\nexport class FormEditorCompiler extends formView.Compiler {\n    compile(key, params = {}) {\n        const xml = this.templates[key];\n\n        // One pass to compute and add the xpath for the arch's node location\n        // onto that node.\n        for (const el of xml.querySelectorAll(interestingSelector)) {\n            const xpath = computeXpath(el);\n            el.setAttribute(\"studioXpath\", xpath);\n        }\n\n        // done after construction of xpaths\n        this.addChatter = true;\n        this.chatterData = {\n            remove_message_ids: false,\n            remove_follower_ids: false,\n            remove_activity_ids: false,\n        };\n        this.avatars = [];\n\n        const mainSheet = xml.querySelector(\"sheet:not(field sheet)\");\n        const isSheetEmpty = !mainSheet || !mainSheet.firstElementChild;\n\n        const compiled = super.compile(key, params);\n\n        const sheetBg = compiled.querySelector(\".o_form_sheet_bg\");\n        if (sheetBg) {\n            let studioHook;\n            if (isSheetEmpty) {\n                studioHook = createElement(\"StudioHook\", {\n                    xpath: `\"${sheetBg.getAttribute(\"studioXpath\")}\"`,\n                    position: \"'inside'\",\n                    type: \"'insideSheet'\",\n                });\n            } else {\n                studioHook = createElement(\"StudioHook\", {\n                    xpath: `\"${sheetBg.getAttribute(\"studioXpath\")}/*[1]\"`,\n                    position: \"'before'\",\n                    type: \"'insideSheet'\",\n                });\n            }\n\n            sheetBg.querySelector(\".o_form_sheet\").prepend(studioHook);\n        }\n\n        if (this.addChatter) {\n            const chatterContainerHook = createElement(\"ChatterContainerHook\", {\n                threadModel: `__comp__.props.record.resModel`,\n                chatterData: objectToString(this.chatterData),\n            });\n            const el = compiled.querySelector(\".o_form_sheet\") || compiled;\n            el.after(chatterContainerHook);\n        } else {\n            const parent = compiled.querySelector(\".o-mail-Form-chatter\");\n            parent.removeAttribute(\"t-attf-class\"); // avoid class o-aside\n            parent.removeAttribute(\"t-if\");\n        }\n\n        const checkStatusBarButtons = compiled.querySelector(\"StatusBarButtons\");\n        if (!checkStatusBarButtons) {\n            const addButtonAction = createElement(\"AddButtonAction\");\n            const el = compiled.querySelector(\".o_form_sheet_bg\") || compiled;\n            el.prepend(addButtonAction);\n        }\n\n        const fieldStatus = compiled.querySelector(`Field[type=\"'statusbar'\"]`); // change selector at some point\n        if (!fieldStatus) {\n            const addStatusBar = !compiled.querySelector(\".o_form_statusbar\");\n            const statusBarFieldHook = createElement(\"StatusBarFieldHook\", { addStatusBar });\n            const el = compiled.querySelector(\".o_form_sheet_bg\") || compiled;\n            el.prepend(statusBarFieldHook);\n        }\n\n        // the button box is included in the control panel, which is not visible in Studio\n        // re-add it to the form view\n        const buttonBoxXml = xml.querySelector(\"div[name='button_box']:not(field div)\");\n        let buttonBox;\n        if (buttonBoxXml) {\n            for (const child of [...buttonBoxXml.children]) {\n                if (child.matches(\"span,field\")) {\n                    buttonBoxXml.removeChild(child);\n                }\n            }\n            buttonBox = this.compileNode(buttonBoxXml, params);\n        } else {\n            buttonBox = createElement(\"ButtonBox\");\n        }\n        const el = compiled.querySelector(\".o_form_sheet_bg\") || compiled;\n        el.prepend(buttonBox);\n\n        const buttonHook = createElement(\n            \"t\",\n            [createElement(\"ButtonHook\", { add_buttonbox: !buttonBoxXml })],\n            { \"t-set-slot\": `slot_button_hook` }\n        );\n\n        buttonBox.insertAdjacentElement(\"afterbegin\", buttonHook);\n\n        // Note: the ribon does not allow to remove an existing avatar!\n        const title = compiled.querySelector(\".oe_title\");\n        if (title) {\n            if (\n                !title.querySelector(\":scope > h1 > [isAvatar]\") && // check it works with <field class=\"oe_avatar\" ... />\n                !title.parentElement.querySelector(\":scope > [isAvatar]\")\n            ) {\n                const avatarHook = createElement(\"AvatarHook\", {\n                    fields: `__comp__.props.record.fields`,\n                });\n                title.before(avatarHook);\n            }\n        }\n        for (const el of this.avatars) {\n            el.removeAttribute(\"isAvatar\");\n        }\n\n        compiled.querySelectorAll(\":not(.o_form_statusbar) Field\").forEach((el) => {\n            el.setAttribute(\"hasEmptyPlaceholder\", \"true\");\n        });\n\n        compiled\n            .querySelectorAll(`InnerGroup > t[t-set-slot][subType=\"'item_component'\"] Field`)\n            .forEach((el) => {\n                el.setAttribute(\"hasLabel\", \"true\");\n            });\n\n        return compiled;\n    }\n\n    applyInvisible(invisible, compiled, params) {\n        return applyInvisible(invisible, compiled, params);\n    }\n\n    createLabelFromField(fieldId, fieldName, fieldString, label, params) {\n        const studioXpath = label.getAttribute(\"studioXpath\");\n        const formLabel = super.createLabelFromField(...arguments);\n        formLabel.setAttribute(\"studioXpath\", `\"${studioXpath}\"`);\n        if (formLabel.hasAttribute(\"t-if\")) {\n            formLabel.setAttribute(\"studioIsVisible\", formLabel.getAttribute(\"t-if\"));\n            formLabel.removeAttribute(\"t-if\");\n        }\n        return formLabel;\n    }\n\n    compileNode(node, params = {}, evalInvisible = true) {\n        const nodeType = node.nodeType;\n        // Put a xpath on the currentSlot containing the future compiled element.\n        // Do it early not to be bothered by recursive call to compileNode.\n        const currentSlot = params.currentSlot;\n        if (nodeType === 1 && currentSlot && !currentSlot.hasAttribute(\"studioXpath\")) {\n            const parentElement = node.parentElement;\n            if (parentElement && parentElement.tagName === \"page\") {\n                const xpath = computeXpath(node.parentElement);\n                currentSlot.setAttribute(\"studioXpath\", `\"${xpath}\"`);\n                // If the page has an OuterGroup as last child, don't add a page studioHook\n                if (!parentElement.querySelector(\":scope > group:last-child > group\")) {\n                    const pageHookProps = {\n                        position: \"'inside'\",\n                        type: \"'page'\",\n                        xpath: `\"${xpath}\"`,\n                    };\n                    currentSlot.setAttribute(\"studioHookProps\", objectToString(pageHookProps));\n                }\n            } else {\n                const xpath = node.getAttribute(\"studioXpath\");\n                currentSlot.setAttribute(\"studioXpath\", `\"${xpath}\"`);\n            }\n        }\n\n        if (nodeType === 1 && node.getAttribute(\"studio_no_fetch\")) {\n            return;\n        }\n\n        const compiled = super.compileNode(node, { ...params, compileInvisibleNodes: true }, true); // always evalInvisible\n\n        if (nodeType === 1) {\n            // Put a xpath on anything of interest.\n            if (node.hasAttribute(\"studioXpath\")) {\n                const xpath = node.getAttribute(\"studioXpath\");\n                if (isComponentNode(compiled)) {\n                    compiled.setAttribute(\"studioXpath\", `\"${xpath}\"`);\n                } else if (!compiled.hasAttribute(\"studioXpath\")) {\n                    compiled.setAttribute(\"studioXpath\", xpath);\n                }\n            }\n\n            if (node.tagName === \"notebook\") {\n                // Since empty pages are not compiled, this compiler has not applied the studioXpath attribute.\n                // We must need to add one as well as the other pages, to make sure we can edit its content properly.\n                const originalChildren = Array.from(node.children).filter(\n                    (e) => e.tagName === \"page\"\n                );\n                Array.from(compiled.children).forEach((elem, index) => {\n                    if (!elem.hasAttribute(\"studioXpath\")) {\n                        const studioXpath = originalChildren[index].getAttribute(\"studioXpath\");\n                        elem.setAttribute(\"studioXpath\", `\"${studioXpath}\"`);\n                        const pageHookProps = {\n                            position: \"'inside'\",\n                            type: \"'page'\",\n                            xpath: `\"${studioXpath}\"`,\n                        };\n                        elem.setAttribute(\"studioHookProps\", objectToString(pageHookProps));\n                    }\n                });\n            }\n\n            if (node.tagName === \"chatter\") {\n                this.addChatter = false;\n                const chatterNode = compiled.querySelector(\n                    \"t[t-component='__comp__.mailComponents.Chatter']\"\n                );\n                const xpath = node.getAttribute(\"studioXpath\");\n                chatterNode.setAttribute(\"studioXpath\", `\"${xpath}\"`);\n                compiled.setAttribute(\"data-studio-xpath\", xpath);\n                compiled.classList.add(\"o-web-studio-editor--element-clickable\");\n            }\n            if (node.classList.contains(\"oe_avatar\")) {\n                compiled.setAttribute(\"isAvatar\", true);\n                this.avatars.push(compiled);\n            }\n\n            if (node.classList.contains(\"o_td_label\") && !node.children.length && !node.textContent.trim()) {\n                compiled.classList.add(\"o-web-studio-editor--element-clickable\");\n                const xpath = node.getAttribute(\"studioXpath\");\n                compiled.setAttribute(\n                    \"t-on-click\",\n                    `(ev) => __comp__.env.config.onNodeClicked(\"${xpath}\")`\n                );\n            }\n\n            const name = node.getAttribute(\"name\"); // not sure that part works\n            if (name === \"message_ids\") {\n                this.chatterData.remove_message_ids = true;\n            } else if (name === \"message_follower_ids\") {\n                this.chatterData.remove_follower_ids = true;\n            } else if (name === \"activity_ids\") {\n                this.chatterData.remove_activity_ids = true;\n            }\n        }\n        return compiled;\n    }\n}\n", "/** @odoo-module */\n\nimport { useState, onMounted, onPatched } from \"@odoo/owl\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { useModelConfigFetchInvisible } from \"@web_studio/client_action/view_editor/editors/utils\";\n\n/**\n * This hook ensures that a record datapoint has the \"parent\" key in its evalContext, allowing\n * to access to field values of the parent record. This is useful in Studio because an x2many\n * record can be opened, but in a standalone fashion. It will be the root of its model, even\n * though, in practice, there's a parent record and a parent form view. This allows snippets like\n * `<field name=\"...\" invisible=\"not parent.id\" />` in the child view to work.\n */\nfunction useExternalParentInModel(model, parentRecord) {\n    model._createRoot = (config, data) => {\n        return new model.constructor.Record(model, config, data, { parentRecord });\n    };\n}\n\nexport class FormEditorController extends formView.Controller {\n    static props = {\n        ...formView.Controller.props,\n        parentRecord: { type: [Object, { value: null }], optional: true },\n    };\n\n    setup() {\n        super.setup();\n        useModelConfigFetchInvisible(this.model);\n        this.mailTemplate = null;\n        this.hasFileViewerInArch = false;\n\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n\n        if (this.props.parentRecord) {\n            useExternalParentInModel(this.model, this.props.parentRecord);\n        }\n\n        onMounted(() => {\n            const xpath = this.viewEditorModel.lastActiveNodeXpath;\n            if (xpath && xpath.includes(\"notebook\")) {\n                const tabXpath = xpath.match(/.*\\/page\\[\\d+\\]/)[0];\n                const tab = document.querySelector(`[data-studio-xpath='${tabXpath}'] a`);\n                if (tab) {\n                    // store the targetted element to restore it after being patched\n                    this.notebookElementData = {\n                        xpath,\n                        restore: Boolean(this.viewEditorModel.activeNodeXpath),\n                        sidebarTab: this.viewEditorModel.sidebarTab,\n                        isTab: xpath.length === tabXpath.length,\n                    };\n                    tab.click();\n                }\n            } else {\n                this.notebookElementData = null;\n            }\n        });\n\n        onPatched(() => {\n            if (this.notebookElementData) {\n                if (\n                    this.notebookElementData.isTab &&\n                    this.viewEditorModel.lastActiveNodeXpath !== this.notebookElementData.xpath\n                ) {\n                    return;\n                }\n                if (this.notebookElementData.restore) {\n                    this.env.config.onNodeClicked(this.notebookElementData.xpath);\n                } else {\n                    // no element was currently highlighted, the editor sidebar must display the stored tab\n                    this.viewEditorModel.resetSidebar(this.notebookElementData.sidebarTab);\n                }\n                this.notebookElementData = null;\n            }\n        });\n    }\n\n    beforeUnload() {}\n\n    _shouldUseSubEnv() {\n        return false;\n    }\n}\n", "/** @odoo-module */\n\nimport { formView } from \"@web/views/form/form_view\";\nimport { studioIsVisible } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { StudioHook } from \"@web_studio/client_action/view_editor/editors/components/studio_hook_component\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nconst components = formView.Renderer.components;\n\n/*\n * Overrides for FormGroups: Probably the trickiest part of all, especially InnerGroup\n * - Append droppable hooks below every visible field, or on empty OuterGroup\n * - Elements deal with invisible themselves\n */\n\n// An utility function that extends the common API parts of groups\nfunction extendGroup(GroupClass) {\n    class Group extends GroupClass {\n        static props = [...GroupClass.props, \"studioXpath?\", \"studioIsVisible?\"];\n        static components = { ...GroupClass.components, StudioHook };\n        setup() {\n            super.setup();\n            this.viewEditorModel = useState(this.env.viewEditorModel);\n            this.rootRef = useRef(\"rootRef\");\n        }\n        get allClasses() {\n            let classes = super.allClasses;\n            if (!studioIsVisible(this.props)) {\n                classes = `${classes || \"\"} o_web_studio_show_invisible`;\n            }\n            if (this.props.studioXpath) {\n                classes = `${classes || \"\"} o-web-studio-editor--element-clickable`;\n            }\n            return classes;\n        }\n        _getItems() {\n            const items = super._getItems();\n            return items.map(([k, v]) => {\n                v = Object.assign({}, v);\n                v.studioIsVisible = v.isVisible;\n                v.isVisible = v.isVisible || this.viewEditorModel.showInvisible;\n                if (v.subType === \"item_component\") {\n                    v.props.studioIsVisible = v.studioIsVisible;\n                    v.props.studioXpath = v.studioXpath;\n                }\n                return [k, v];\n            });\n        }\n\n        onGroupClicked(ev) {\n            if (ev.target.closest(\".o-web-studio-editor--element-clickable\") !== this.rootRef.el) {\n                return;\n            }\n            this.env.config.onNodeClicked(this.props.studioXpath);\n        }\n    }\n    return Group;\n}\n\n// A component to display fields with an automatic label.\n// Those are the only ones (for now), to be draggable internally\n// It should shadow the Field and its Label below\nclass InnerGroupItemComponent extends Component {\n    static template = \"web_studio.Form.InnerGroup.ItemComponent\";\n    static props = {\n        cell: { type: Object },\n        slots: { type: Object },\n    };\n    setup() {\n        const labelRef = useRef(\"labelRef\");\n        const fieldRef = useRef(\"fieldRef\");\n\n        this.labelRef = labelRef;\n\n        useEffect(\n            (studioIsVisible, labelEl, fieldEl) => {\n                // Only label act as the business unit for studio\n                if (labelEl) {\n                    const clickable = labelEl.querySelector(\n                        \".o-web-studio-editor--element-clickable\"\n                    );\n                    if (clickable) {\n                        clickable.classList.remove(\"o-web-studio-editor--element-clickable\");\n                    }\n                    labelEl.classList.add(\"o-web-studio-editor--element-clickable\");\n                    const invisible = labelEl.querySelector(\".o_web_studio_show_invisible\");\n                    if (invisible) {\n                        invisible.classList.remove(\"o_web_studio_show_invisible\");\n                    }\n                    labelEl.classList.toggle(\"o_web_studio_show_invisible\", !studioIsVisible);\n                    labelEl.classList.add(\"o-draggable\");\n                }\n\n                if (fieldEl) {\n                    const clickable = fieldEl.querySelector(\n                        \".o-web-studio-editor--element-clickable\"\n                    );\n                    if (clickable) {\n                        clickable.classList.remove(\"o-web-studio-editor--element-clickable\");\n                    }\n                    const invisible = fieldEl.querySelector(\".o_web_studio_show_invisible\");\n                    if (invisible) {\n                        invisible.classList.remove(\"o_web_studio_show_invisible\");\n                    }\n                    fieldEl.classList.add(\"o-web-studio-element-ghost\");\n                }\n            },\n            () => [this.cell.studioIsVisible, labelRef.el, fieldRef.el]\n        );\n\n        this.onMouseFieldIO = (ev) => {\n            labelRef.el.classList.toggle(\"o-web-studio-ghost-hovered\", ev.type === \"mouseover\");\n        };\n    }\n    get cell() {\n        return this.props.cell;\n    }\n\n    onClicked(ev) {\n        if (ev.target.closest(\".o-web-studio-element-ghost\")) {\n            ev.stopPropagation();\n        }\n        this.env.config.onNodeClicked(this.cell.studioXpath);\n    }\n}\n\nconst _InnerGroup = extendGroup(components.InnerGroup);\nexport class InnerGroup extends _InnerGroup {\n    static template = \"web_studio.Form.InnerGroup\";\n    getRows() {\n        const rows = super.getRows();\n        if (!this.viewEditorModel.showInvisible) {\n            rows.forEach((row) => {\n                row.isVisible = row.some((cell) => cell.studioIsVisible);\n            });\n        }\n        return rows;\n    }\n\n    getStudioHooks() {\n        const hooks = new Map();\n        const rows = this.getRows();\n        const hasRows = rows.length >= 1 && rows[0].length;\n\n        if (!hasRows) {\n            hooks.set(\"inside\", {\n                xpath: this.props.studioXpath,\n                position: \"inside\",\n                subTemplate: \"formGrid\",\n                colSpan: this.props.maxCols,\n            });\n        }\n\n        for (const rowIdx in rows) {\n            const row = rows[rowIdx];\n            const colSpan = row.reduce((acc, val) => acc + val.itemSpan || 1, 0);\n            if (!hooks.has(\"beforeFirst\")) {\n                const cell = row[0];\n                if (cell) {\n                    hooks.set(\"beforeFirst\", {\n                        xpath: cell.studioXpath,\n                        position: \"before\",\n                        subTemplate: \"formGrid\",\n                        width: cell.width,\n                        colSpan,\n                    });\n                }\n            }\n\n            if (row.every((cell) => !cell.studioIsVisible) && !this.viewEditorModel.showInvisible) {\n                continue;\n            }\n            const cell = row[row.length - 1];\n            if (cell) {\n                hooks.set(`afterRow ${rowIdx}`, {\n                    xpath: cell.studioXpath,\n                    position: \"after\",\n                    subTemplate: \"formGrid\",\n                    width: cell.width,\n                    colSpan,\n                });\n            }\n        }\n        return hooks;\n    }\n}\n\nInnerGroup.components.InnerGroupItemComponent = InnerGroupItemComponent;\n\n// Simple override for OuterGroups\nexport const OuterGroup = extendGroup(components.OuterGroup);\nOuterGroup.template = \"web_studio.Form.OuterGroup\";\n", "/** @odoo-module */\n\nimport { useRef, useEffect, useState } from \"@odoo/owl\";\nimport { formView } from \"@web/views/form/form_view\";\nimport * as formEditorRendererComponents from \"@web_studio/client_action/view_editor/editors/form/form_editor_renderer/form_editor_renderer_components\";\n\nimport { ChatterContainer, ChatterContainerHook } from \"../chatter_container\";\nimport { StudioHook } from \"@web_studio/client_action/view_editor/editors/components/studio_hook_component\";\nimport { FieldStudio } from \"@web_studio/client_action/view_editor/editors/components/field_studio\";\nimport { WidgetStudio } from \"@web_studio/client_action/view_editor/editors/components/widget_studio\";\nimport { ViewButtonStudio } from \"@web_studio/client_action/view_editor/editors/components/view_button_studio\";\nimport { InnerGroup, OuterGroup } from \"./form_editor_groups\";\nimport { AddButtonAction } from \"@web_studio/client_action/view_editor/interactive_editor/action_button/action_button\";\n\nclass Setting extends formView.Renderer.components.Setting {\n    static props = {\n        ...formView.Renderer.components.Setting.props,\n        studioXpath: { type: String, optional: true },\n        studioIsVisible: { type: Boolean, optional: true },\n    };\n}\nexport class FormEditorRenderer extends formView.Renderer {\n    static components = {\n        ...formView.Renderer.components,\n        ...formEditorRendererComponents,\n        Field: FieldStudio,\n        Widget: WidgetStudio,\n        ViewButton: ViewButtonStudio,\n        ChatterContainerHook,\n        InnerGroup,\n        OuterGroup,\n        StudioHook,\n        Setting,\n        AddButtonAction,\n    };\n    setup() {\n        super.setup();\n        const rootRef = useRef(\"compiled_view_root\");\n        this.rootRef = rootRef;\n        const viewEditorModel = this.env.viewEditorModel;\n        this.viewEditorModel = useState(viewEditorModel);\n        this.mailComponents.Chatter = ChatterContainer;\n\n        // Deals with invisible modifier by reacting to config.studioShowVisible.\n        useEffect(\n            (rootEl, showInvisible) => {\n                if (!rootEl) {\n                    return;\n                }\n                rootEl.classList.add(\"o_web_studio_form_view_editor\");\n                if (showInvisible) {\n                    rootEl\n                        .querySelectorAll(\":not(.o-mail-Form-chatter) .o_invisible_modifier\")\n                        .forEach((el) => {\n                            el.classList.add(\"o_web_studio_show_invisible\");\n                            el.classList.remove(\"o_invisible_modifier\");\n                        });\n                } else {\n                    rootEl\n                        .querySelectorAll(\":not(.o-mail-Form-chatter) .o_web_studio_show_invisible\")\n                        .forEach((el) => {\n                            el.classList.remove(\"o_web_studio_show_invisible\");\n                            el.classList.add(\"o_invisible_modifier\");\n                        });\n                }\n            },\n            () => [rootRef.el, viewEditorModel.showInvisible]\n        );\n\n        // do this in another way?\n        useEffect(\n            (rootEl) => {\n                if (rootEl) {\n                    const optCols = rootEl.querySelectorAll(\"i.o_optional_columns_dropdown_toggle\");\n                    for (const col of optCols) {\n                        col.classList.add(\"text-muted\");\n                    }\n                }\n            },\n            () => [rootRef.el]\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { formView } from \"@web/views/form/form_view\";\nimport { StudioHook } from \"@web_studio/client_action/view_editor/editors/components/studio_hook_component\";\nimport { NewButtonBoxDialog } from \"@web_studio/client_action/view_editor/editors/form/form_editor_sidebar/properties/button_properties/new_button_box_dialog\";\nimport { FieldSelectorDialog } from \"@web_studio/client_action/view_editor/editors/components/field_selector_dialog\";\nimport { SelectionContentDialog } from \"@web_studio/client_action/view_editor/interactive_editor/field_configuration/selection_content_dialog\";\nimport {\n    randomName,\n    studioIsVisible,\n    useStudioRef,\n} from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, useRef } from \"@odoo/owl\";\nimport { AddButtonAction } from \"../../../interactive_editor/action_button/action_button\";\n\n/**\n * Overrides and extensions of components used by the FormRenderer\n * As a rule of thumb, elements should be able to handle the props\n * - studioXpath: the xpath to the node in the form's arch to which the component\n *   refers\n * - They generally be clicked on to change their characteristics (in the Sidebar)\n * - The click doesn't trigger default behavior (the view is inert)\n * - They can be draggable (FormLabel referring to a field)\n * - studioIsVisible: all components whether invisible or not, are compiled and rendered\n *   this props allows to toggle the class o_invisible_modifier\n * - They can have studio hooks, that are placeholders for dropping content (new elements, field, or displace elements)\n */\n\nconst components = formView.Renderer.components;\n\n/*\n * FormLabel:\n * - Can be draggable if in InnerGroup\n */\nexport class FormLabel extends components.FormLabel {\n    static template = \"web_studio.FormLabel\";\n    static props = {\n        ...components.FormLabel.props,\n        studioXpath: String,\n        studioIsVisible: { type: Boolean, optional: true },\n    };\n    setup() {\n        super.setup();\n        useStudioRef(\"rootRef\", this.onClick);\n    }\n    get className() {\n        let className = super.className;\n        if (!studioIsVisible(this.props)) {\n            className += \" o_web_studio_show_invisible\";\n        }\n        className += \" o-web-studio-editor--element-clickable\";\n        return className;\n    }\n    onClick(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        this.env.config.onNodeClicked(this.props.studioXpath);\n    }\n}\n\n/*\n * Notebook:\n * - Display every page, the elements in the page handle whether they are invisible themselves\n * - Push a droppable hook on every empty page\n * - Can add a new page\n */\nexport class Notebook extends components.Notebook {\n    static template = \"web_studio.Notebook.Hook\";\n    static components = { ...components.Notebook.components, StudioHook };\n    static props = {\n        ...components.Notebook.props,\n        studioIsVisible: { type: Boolean, optional: true },\n        studioXpath: String,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        super.setup();\n    }\n    computePages(props) {\n        const pages = super.computePages(props);\n        pages.forEach((p) => {\n            p[1].studioIsVisible = p[1].isVisible;\n            p[1].isVisible = p[1].isVisible || this.viewEditorModel.showInvisible;\n        });\n        return pages;\n    }\n    onNewPageClicked() {\n        const vem = this.viewEditorModel;\n        const node = {\n            tag: \"page\",\n            attrs: {\n                string: _t(\"New Page\"),\n                name: randomName(\"studio_page\"),\n            },\n        };\n        vem.doOperation({\n            type: \"add\",\n            node,\n            target: vem.getFullTarget(this.props.studioXpath),\n            position: \"inside\",\n        });\n    }\n}\n\nexport class StatusBarButtons extends components.StatusBarButtons {\n    static template = `web_studio.FormViewAddButtonAction`;\n    static components = {\n        ...components.StatusBarButtons.components,\n        AddButtonAction,\n    };\n}\n\nexport class StatusBarFieldHook extends Component {\n    static template = \"web_studio.AddElementHook\";\n    static props = {\n        addStatusBar: { type: Boolean },\n    };\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n    get classNames() {\n        return \"o_web_studio_statusbar_hook\";\n    }\n    get title() {\n        return _t(\"Add a pipeline status bar\");\n    }\n    onClick() {\n        this.addDialog(SelectionContentDialog, {\n            defaultChoices: [\n                [\"status1\", _t(\"First Status\")],\n                [\"status2\", _t(\"Second Status\")],\n                [\"status3\", _t(\"Third Status\")],\n            ],\n            onConfirm: (choices) => {\n                const viewEditorModel = this.env.viewEditorModel;\n                if (this.props.addStatusBar) {\n                    viewEditorModel.pushOperation({ type: \"statusbar\" });\n                }\n\n                const target = {\n                    tag: \"header\",\n                };\n                const subViewXpath = viewEditorModel.getSubviewXpath();\n                if (subViewXpath) {\n                    target.subview_xpath = subViewXpath;\n                }\n\n                viewEditorModel.doOperation({\n                    type: \"add\",\n                    target,\n                    position: \"inside\",\n                    node: {\n                        attrs: {\n                            widget: \"statusbar\",\n                            options: \"{'clickable': '1'}\",\n                        },\n                        field_description: {\n                            default_value: true,\n                            field_description: _t(\"Pipeline status bar\"),\n                            model_name: viewEditorModel.resModel,\n                            name: randomName(`x_studio_selection_field`),\n                            selection: JSON.stringify(choices),\n                            type: \"selection\",\n                        },\n                        tag: \"field\",\n                    },\n                });\n            },\n        });\n    }\n}\n\nexport class AvatarHook extends Component {\n    static template = \"web_studio.AddElementHook\";\n    static props = { fields: Object };\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n    get classNames() {\n        return \"oe_avatar ms-3 o_web_studio_avatar\";\n    }\n    get title() {\n        return _t(\"Add Picture\");\n    }\n    onClick() {\n        const fields = [];\n        for (const field of Object.values(this.props.fields)) {\n            if (field.type === \"binary\") {\n                fields.push(field);\n            }\n        }\n        this.addDialog(FieldSelectorDialog, {\n            fields,\n            showNew: true,\n            onConfirm: (field) => {\n                this.env.viewEditorModel.doOperation({\n                    type: \"avatar_image\",\n                    field,\n                });\n            },\n        });\n    }\n}\n\nexport class ButtonHook extends Component {\n    static template = \"web_studio.AddElementHook\";\n    static props = {\n        add_buttonbox: { type: Boolean, optional: true },\n        studioIsVisible: { type: Boolean, optional: true },\n    };\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n    get classNames() {\n        return \"oe_stat_button o_web_studio_button_hook flex-grow-1 flex-lg-grow-0 fa fa-plus-square\";\n    }\n    get tooltip() {\n        return _t(\"Add a button\");\n    }\n    onClick() {\n        this.addDialog(NewButtonBoxDialog, {\n            model: this.env.viewEditorModel,\n            isAddingButtonBox: Boolean(this.props.add_buttonbox),\n        });\n    }\n}\n\nexport class ButtonBox extends components.ButtonBox {\n    static template = \"web_studio.ButtonBox\";\n    static components = {};\n    static props = {\n        ...components.ButtonBox.props,\n        studioIsVisible: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.togglerRef = useRef(\"toggleRef\");\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.expanded = useState({ value: false });\n    }\n\n    toggle() {\n        this.expanded.value = !this.expanded.value;\n        this.togglerRef.el.classList.toggle(\"show\", this.expanded.value);\n        this.togglerRef.el.ariaExpanded = this.expanded.value;\n    }\n\n    isSlotVisible(slot) {\n        if (this.viewEditorModel.isEditingSubview) {\n            return false;\n        }\n        return this.viewEditorModel.showInvisible || super.isSlotVisible(slot);\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport {\n    ExistingFields,\n    NewFields,\n} from \"@web_studio/client_action/view_editor/editors/components/view_fields\";\nimport { ViewStructures } from \"@web_studio/client_action/view_editor/editors/components/view_structures\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { Properties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/properties\";\nimport { ButtonProperties } from \"@web_studio/client_action/view_editor/editors/form/form_editor_sidebar/properties/button_properties/button_properties\";\nimport { FieldProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/field_properties/field_properties\";\nimport { GroupProperties } from \"@web_studio/client_action/view_editor/editors/form/form_editor_sidebar/properties/group_properties/group_properties\";\nimport { LabelProperties } from \"@web_studio/client_action/view_editor/editors/form/form_editor_sidebar/properties/label_properties/label_properties\";\nimport { PageProperties } from \"@web_studio/client_action/view_editor/editors/form/form_editor_sidebar/properties/page_properties/page_properties\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { ChatterProperties } from \"@web_studio/client_action/view_editor/editors/form/form_editor_sidebar/properties/chatter_properties/chatter_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { WidgetProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/widget_properties/widget_properties\";\nimport { OTdLabelProperties } from \"./properties/o_td_label_properties/o_td_label_properties\";\n\nexport class FormEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.FormEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        NewFields,\n        ExistingFields,\n        Property,\n        Properties,\n        ViewStructures,\n        SidebarViewToolbox,\n    };\n    static get viewStructures() {\n        return {\n            notebook: {\n                name: _t(\"Tabs\"),\n                class: \"o_web_studio_field_tabs\",\n            },\n            group: {\n                name: _t(\"Column\"),\n                class: \"o_web_studio_field_columns\",\n            },\n        };\n    }\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n        this.propertiesComponents = {\n            button: {\n                component: ButtonProperties,\n                props: {\n                    availableOptions: [\"invisible\"],\n                },\n            },\n            field: {\n                component: FieldProperties,\n                props: {\n                    availableOptions: [\"invisible\", \"required\", \"readonly\", \"string\", \"help\"],\n                },\n            },\n            group: {\n                component: GroupProperties,\n            },\n            label: {\n                component: LabelProperties,\n            },\n            page: {\n                component: PageProperties,\n            },\n            chatter: {\n                component: ChatterProperties,\n            },\n            widget: {\n                component: WidgetProperties,\n            },\n            div: {\n                component: OTdLabelProperties,\n            },\n        };\n    }\n\n    get activeActions() {\n        return this.viewEditorModel.controllerProps.archInfo.activeActions;\n    }\n\n    getActiveAction(name) {\n        return this.activeActions[name] === true;\n    }\n\n    onAttributeChanged(value, name) {\n        return this.editArchAttributes({ [name]: value });\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Record } from \"@web/model/record\";\nimport { Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { Many2ManyTagsField } from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { ClassAttribute } from \"@web_studio/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { RainbowEffect } from \"./rainbow_effect\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { useSnackbarWrapper } from \"@web_studio/client_action/view_editor/view_editor_hook\";\nimport { ModifiersProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/modifiers/modifiers_properties\";\nimport { buildApprovalKey } from \"@web_studio/approval/approval_hook\";\n\nexport class ButtonProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Button\";\n    static props = {\n        node: { type: Object },\n        availableOptions: { type: Array, optional: true },\n    };\n    static components = {\n        CheckBox,\n        ClassAttribute,\n        Many2OneField,\n        Many2ManyTagsField,\n        RainbowEffect,\n        Record,\n        SelectMenu,\n        Property,\n        ViewStructureProperties,\n        ModifiersProperties,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.state = useState({});\n        this.editNodeAttributes = useEditNodeAttributes();\n\n        this.decoratedOrmCall = useSnackbarWrapper(this.orm.call.bind(this.orm));\n        this.decoratedOrmWrite = useSnackbarWrapper(this.orm.write.bind(this.orm));\n\n        this.domainResUsers = [\n            [\"id\", \"not in\", [1]],\n            [\"share\", \"=\", false],\n        ];\n        const m2mFieldsToFetch = {\n            display_name: { type: \"char\" },\n        };\n        const approvalRecordDefinition = {\n            approval_group_id: {\n                type: \"many2one\",\n                relation: \"res.groups\",\n                domain: [[\"share\", \"=\", false]],\n            },\n            approver_ids: {\n                type: \"many2many\",\n                relation: \"res.users\",\n                related: { activeFields: m2mFieldsToFetch, fields: m2mFieldsToFetch },\n            },\n            users_to_notify: {\n                type: \"many2many\",\n                relation: \"res.users\",\n                related: { activeFields: m2mFieldsToFetch, fields: m2mFieldsToFetch },\n            },\n        };\n        this.recordProps = {\n            resModel: \"studio.approval.rule\",\n            fields: approvalRecordDefinition,\n            activeFields: approvalRecordDefinition,\n        };\n\n        onWillStart(() => {\n            this.updateApprovalSpec();\n        });\n\n        onWillUpdateProps((nextProps) => {\n            this.updateApprovalSpec(this.getApprovalParams(nextProps.node));\n        });\n    }\n\n    isValid(fieldName, record) {\n        if ([\"approver_ids\", \"approval_group_id\"].includes(fieldName)) {\n            const evalContext = record.evalContext;\n            return evalContext.approver_ids.length || evalContext.approval_group_id;\n        }\n        return true;\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n\n    async onChangeApprovalRecord(record, changes, id) {\n        await this.decoratedOrmWrite(\"studio.approval.rule\", [id], changes);\n        this.updateApprovalSpec();\n    }\n\n    get showRainbowMan() {\n        const attrs = this.props.node.attrs;\n        return attrs.class !== \"oe_stat_button\" && attrs.type === \"object\";\n    }\n\n    async createApprovalRule() {\n        const params = this.getApprovalParams();\n        if (this.state.approvalSpec?.rules.length) {\n            const orders = this.state.approvalSpec.rules.map((id) =>\n                parseInt(this.state.allRules[id][\"notification_order\"])\n            );\n            params.push(Math.min(Math.max(...orders) + 1, 9).toString());\n        }\n        await this.decoratedOrmCall(\"studio.approval.rule\", \"create_rule\", params);\n        this.updateApprovalSpec();\n    }\n\n    getApprovalParams(node = this.props.node) {\n        let method,\n            action = false;\n        if (node.attrs.type === \"object\") {\n            method = node.attrs.name;\n        } else {\n            action = node.attrs.name;\n        }\n        return [this.env.viewEditorModel.resModel, method, action];\n    }\n\n    async getApprovalSpec(approvalParams) {\n        const approvalParamsObject = {\n            model: approvalParams[0],\n            method: approvalParams[1],\n            action_id: approvalParams[2],\n        };\n        const approvals = await this.env.services[\"web_studio.get_approval_spec_batched\"](\n            approvalParamsObject\n        );\n        return approvals;\n    }\n\n    async onApprovalArchive(id) {\n        await this.decoratedOrmWrite(\"studio.approval.rule\", [id], {\n            active: false,\n        });\n        this.updateApprovalSpec();\n    }\n\n    async onApprovalEdit(name, id, value) {\n        const isMethod = this.props.node.attrs.type === \"object\";\n        await rpc(\"/web_studio/edit_approval\", {\n            model: this.env.viewEditorModel.resModel,\n            method: isMethod ? this.props.node.attrs.name : false,\n            action: isMethod ? false : this.props.node.attrs.name,\n            operations: [[name, id, value]],\n        });\n        this.updateApprovalSpec();\n    }\n\n    onApprovalSelectDomain(id) {\n        const rule = this.state.allRules[id];\n        const domain = rule.domain;\n        this.dialog.add(DomainSelectorDialog, {\n            resModel: this.env.viewEditorModel.resModel,\n            domain: JSON.stringify(domain || []),\n            isDebugMode: !!this.env.debug,\n            onConfirm: async (domain) => {\n                await this.decoratedOrmWrite(\"studio.approval.rule\", [id], {\n                    domain,\n                });\n                this.updateApprovalSpec();\n            },\n        });\n    }\n\n    async onChangeNotificationOrder(ev, id) {\n        await this.decoratedOrmWrite(\"studio.approval.rule\", [id], {\n            notification_order: ev.target.value,\n        });\n    }\n\n    async updateApprovalSpec(params = this.getApprovalParams()) {\n        this.env.viewEditorModel.env.bus.trigger(\"approval-update\");\n        const approvalSpec = await this.getApprovalSpec(params);\n        this.state.allRules = approvalSpec.all_rules;\n        const approvalKey = buildApprovalKey(false, params[1] || false, params[2] || false);\n        this.state.approvalSpec = approvalSpec[params[0]][approvalKey] || {\n            rules: [],\n            entries: [],\n        };\n    }\n\n    async openKanbanApprovalRules() {\n        const [resModel, method, action] = this.getApprovalParams();\n        return this.actionService.doActionButton({\n            context: {\n                studio: true,\n            },\n            type: \"object\",\n            name: \"open_kanban_rules\",\n            resModel: \"studio.approval.rule\",\n            resIds: [],\n            args: JSON.stringify([resModel, method, action]),\n            stackPosition: \"replaceCurrentAction\",\n        });\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FontAwesomeIconSelector } from \"@web_studio/client_action/components/font_awesome_icon_selector/font_awesome_icon_selector\";\n\nexport class NewButtonBoxDialog extends Component {\n    static template = \"web_studio.NewButtonBoxDialog\";\n    static components = {\n        Dialog,\n        FontAwesomeIconSelector,\n        Many2XAutocomplete,\n    };\n    static props = {\n        isAddingButtonBox: { type: Boolean },\n        model: { type: Object },\n        close: { type: Function },\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            icon: \"fa fa-diamond\",\n            field: undefined,\n        });\n        this.text = undefined;\n    }\n    async update(selection) {\n        if (!selection[0].display_name) {\n            const resId = selection[0].id;\n            const fields = [\"display_name\"];\n            const record = await this.orm.read(\"ir.model.fields\", [resId], fields);\n            selection[0].display_name = record[0].display_name;\n        }\n        this.state.field = selection[0];\n    }\n    getDomain() {\n        return [\n            [\"relation\", \"=\", this.props.model.resModel],\n            [\"ttype\", \"in\", [\"many2one\", \"many2many\"]],\n            [\"store\", \"=\", true],\n        ];\n    }\n    onConfirm() {\n        if (!this.state.field?.id) {\n            return this.notification.add(_t(\"Select a related field.\"));\n        }\n        if (this.props.isAddingButtonBox) {\n            this.props.model.pushOperation({ type: \"buttonbox\" });\n        }\n        this.props.model.doOperation({\n            type: \"add\",\n            target: {\n                tag: \"div\",\n                attrs: {\n                    class: \"oe_button_box\",\n                },\n            },\n            position: \"inside\",\n            node: {\n                tag: \"button\",\n                field: this.state.field.id,\n                string: this.text || _t(\"New button\"),\n                attrs: {\n                    class: \"oe_stat_button\",\n                    // we don't need the 'fa' class here, but only the icon class\n                    icon: this.state.icon.slice(3),\n                },\n            },\n        });\n        this.props.close();\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\n\nexport class RainbowEffect extends Component {\n    static template = \"web_studio.ViewEditorSidebar.RainbowEffect\";\n    static props = {\n        effect: { type: true, optional: true },\n        onChange: { type: Function },\n    };\n    static components = {\n        FileInput,\n        SelectMenu,\n        Property,\n    };\n    setup() {\n        this.user = user;\n    }\n    get choices() {\n        return [\n            { label:  _t(\"Fast\"), value: \"fast\" },\n            { label:  _t(\"Medium\"), value: \"medium\" },\n            { label:  _t(\"Slow\"), value: \"slow\" },\n            { label:  _t(\"None\"), value: \"no\" },\n        ];\n    }\n    get rainbowEffect() {\n        const effect = this.props.effect;\n        if (effect === undefined) {\n            return null;\n        }\n        if (effect === \"True\") {\n            return {};\n        }\n        return evaluateExpr(effect);\n    }\n    onRainbowEffectChange(name, value) {\n        const effect = this.rainbowEffect;\n        if (!value || !value.length) {\n            delete effect[name];\n        } else {\n            effect[name] = value;\n        }\n        this.props.onChange(effect, \"effect\");\n    }\n    toggleRainbowMan() {\n        const effect = this.rainbowEffect;\n        const newValue = effect ? \"False\" : \"{}\";\n        this.props.onChange(newValue, \"effect\");\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, useState, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\n\nexport class ChatterProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Chatter\";\n    static components = { Property, SidebarPropertiesToolbox };\n    static props = [\"node\"];\n\n    setup() {\n        this.state = useState({});\n\n        onWillStart(async () => {\n            const alias = await this.getMailAlias(this.props.node);\n            this.state.mailAlias = alias.email_alias;\n            this.state.aliasDomain = alias.alias_domain;\n        });\n\n        onWillUpdateProps(async (nextProps) => {\n            const alias = await this.getMailAlias(nextProps.node);\n            this.state.mailAlias = alias.email_alias;\n            this.state.aliasDomain = alias.alias_domain;\n        });\n    }\n\n    async getMailAlias(node) {\n        const mailAliasObj = await rpc(\"/web_studio/get_email_alias\", {\n            model_name: this.env.viewEditorModel.resModel,\n        });\n        return mailAliasObj;\n    }\n\n    onChangeMailAlias(value) {\n        rpc(\"/web_studio/set_email_alias\", {\n            model_name: this.env.viewEditorModel.resModel,\n            value,\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { ModifiersProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/modifiers/modifiers_properties\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class GroupProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Group\";\n    static components = {\n        ModifiersProperties,\n        Property,\n        ViewStructureProperties,\n    };\n    static props = [\"node\"];\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class LabelProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Label\";\n    static components = { Property, SidebarPropertiesToolbox };\n    static props = [\"node\"];\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\n\nexport class OTdLabelProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.OTdLabelProperties\";\n    static components = { SidebarPropertiesToolbox };\n    static props = [\"node\"];\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { LimitGroupVisibility } from \"@web_studio/client_action/view_editor/interactive_editor/properties/limit_group_visibility/limit_group_visibility\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\nimport { ModifiersProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/modifiers/modifiers_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nclass PageNodeToolbox extends SidebarPropertiesToolbox {\n    removeNodeFromArch() {\n        const node = this.node;\n        let xpathToRemove = node.xpath;\n        if (node.arch.parentElement.children.length <= 1) {\n            // retarget to the parent notebook\n            xpathToRemove = node.xpath.split(\"/\").slice(0, -1).join(\"/\");\n        }\n        return super.removeNodeFromArch(xpathToRemove);\n    }\n}\n\nexport class PageProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Page\";\n    static components = {\n        Property,\n        LimitGroupVisibility,\n        PageNodeToolbox,\n        ModifiersProperties,\n    };\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n\n    static props = {\n        node: { type: Object },\n    }\n}\n", "import { ganttView } from \"@web_gantt/gantt_view\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class GanttEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.GanttEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        Property,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n    }\n\n    get colorChoices() {\n        return this.modelParams.decorationFields.map((value) => {\n            return {\n                label: this.modelParams.fields[value].string,\n                value,\n            };\n        });\n    }\n\n    get currentDayPrecision() {\n        return this.dayPrecisionChoices.find((e) => e.value === this.precisionValues.day)?.value;\n    }\n\n    get currentWeekPrecision() {\n        return this.weekAndMonthPrecisionChoices.find((e) => e.value === this.precisionValues.week)\n            ?.value;\n    }\n\n    get currentMonthPrecision() {\n        return this.weekAndMonthPrecisionChoices.find((e) => e.value === this.precisionValues.month)\n            ?.value;\n    }\n\n    get dayPrecisionChoices() {\n        return [\n            { label: _t(\"Quarter Hour\"), value: \"hour:quarter\" },\n            { label: _t(\"Half Hour\"), value: \"hour:half\" },\n            { label: _t(\"Hour\"), value: \"hour:full\" },\n        ];\n    }\n\n    get defaultOrderChoices() {\n        return [\n            { value: \"asc\", label: _t(\"Ascending\") },\n            { value: \"desc\", label: _t(\"Descending\") },\n        ];\n    }\n\n    get defaultScalesChoices() {\n        const allowedScales = Object.keys(this.modelParams.scales);\n        return [\n            { value: \"day\", label: _t(\"Day\") },\n            { value: \"week\", label: _t(\"Week\") },\n            { value: \"week_2\", label: _t(\"Week (expanded)\") },\n            { value: \"month\", label: _t(\"Month\") },\n            { value: \"month_3\", label: _t(\"Month (expanded)\") },\n            { value: \"year\", label: _t(\"Year\") },\n        ].filter((e) => allowedScales.includes(e.value));\n    }\n\n    get displayModeChoices() {\n        return [\n            { label: _t(\"Dense\"), value: \"dense\" },\n            { label: _t(\"Sparse\"), value: \"sparse\" },\n        ];\n    }\n\n    get fieldsChoices() {\n        return Object.values(this.modelParams.fields)\n            .filter((f) => f.store && this.viewEditorModel.GROUPABLE_TYPES.includes(f.type))\n            .map((f) => {\n                return {\n                    label: f.string,\n                    value: f.name,\n                };\n            });\n    }\n\n    get fieldsDateChoices() {\n        return Object.values(this.modelParams.fields)\n            .filter((f) => f.store && [\"date\", \"datetime\"].includes(f.type))\n            .map((f) => {\n                return {\n                    label: f.string,\n                    value: f.name,\n                };\n            });\n    }\n\n    get modelParams() {\n        return this.viewEditorModel.controllerProps.modelParams.metaData;\n    }\n\n    get weekAndMonthPrecisionChoices() {\n        return [\n            { label: _t(\"Half Day\"), value: \"day:half\" },\n            { label: _t(\"Day\"), value: \"day:full\" },\n        ];\n    }\n\n    get precisionValues() {\n        const precision =\n            this.viewEditorModel.xmlDoc\n                .querySelector(\"gantt\")\n                .getAttribute(\"precision\")\n                ?.replace(/'/g, '\"') || \"{}\";\n        return JSON.parse(precision);\n    }\n\n    onDefaultGroupByChanged(selection) {\n        this.onViewAttributeChanged(selection.join(\",\"), \"default_group_by\");\n    }\n\n    onPrecisionChanged(value, name) {\n        const precision = this.precisionValues;\n        precision[name] = value;\n        this.onViewAttributeChanged(JSON.stringify(precision), \"precision\");\n    }\n\n    onViewAttributeChanged(value, name) {\n        return this.editArchAttributes({ [name]: value });\n    }\n}\n\nregistry.category(\"studio_editors\").add(\"gantt\", {\n    ...ganttView,\n    Sidebar: GanttEditorSidebar,\n});\n", "/** @odoo-module */\n\nimport { Component, onWillPatch, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\n\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport * as operationUtils from \"@web_studio/client_action/view_editor/operations_utils\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class GraphEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.GraphEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        Property,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n\n        onWillPatch(() => {\n            this.oldFieldValues = {\n                firstDimension: this.modelParams.groupBy[0],\n                secondDimension: this.modelParams.groupBy[1],\n                measure:\n                    this.modelParams.measure === \"__count\" ? undefined : this.modelParams.measure,\n            };\n        });\n    }\n\n    onViewAttributeChanged(value, name) {\n        value = value ? value : \"\";\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    onGroupByChanged(type, newField, oldField) {\n        const operation = operationUtils.viewGroupByOperation(\"graph\", type, newField, oldField);\n        this.viewEditorModel.doOperation(operation);\n    }\n\n    get modelParams() {\n        return this.viewEditorModel.controllerProps.modelParams;\n    }\n\n    get typeChoices() {\n        return [\n            { label: _t(\"Bar\"), value: \"bar\" },\n            { label: _t(\"Line\"), value: \"line\" },\n            { label: _t(\"Pie\"), value: \"pie\" },\n        ];\n    }\n\n    get orderChoices() {\n        return [\n            { label: _t(\"Ascending\"), value: \"asc\" },\n            { label: _t(\"Descending\"), value: \"desc\" },\n        ];\n    }\n\n    get firstGroupbyChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) => field.store && field.name !== this.modelParams.groupBy[1]\n        );\n    }\n\n    get secondGroupbyChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) => field.store && field.name !== this.modelParams.groupBy[0]\n        );\n    }\n\n    get mesureChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            [\"integer\", \"float\", \"monetary\"],\n            (field) => field.store && field.name !== \"id\"\n        );\n    }\n}\n\nregistry.category(\"studio_editors\").add(\"graph\", {\n    ...graphView,\n    Sidebar: GraphEditorSidebar,\n});\n", "import { registry } from \"@web/core/registry\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanEditorCompiler } from \"./kanban_editor_compiler\";\nimport { KanbanEditorRecord } from \"@web_studio/client_action/view_editor/editors/kanban/kanban_editor_record\";\nimport { KanbanEditorRenderer } from \"@web_studio/client_action/view_editor/editors/kanban/kanban_editor_renderer\";\nimport { makeModelErrorResilient } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { KanbanEditorSidebar } from \"./kanban_editor_sidebar/kanban_editor_sidebar\";\nimport { getStudioNoFetchFields, useModelConfigFetchInvisible } from \"../utils\";\nimport { KANBAN_CARD_ATTRIBUTE } from \"@web/views/kanban/kanban_arch_parser\";\n\nclass EditorArchParser extends kanbanView.ArchParser {\n    parse(arch, models, modelName) {\n        const parsed = super.parse(...arguments);\n        const noFetch = getStudioNoFetchFields(parsed.fieldNodes);\n        parsed.fieldNodes = omit(parsed.fieldNodes, ...noFetch.fieldNodes);\n        return parsed;\n    }\n}\nclass OneRecordModel extends kanbanView.Model {\n    async load() {\n        this.progressAttributes = false;\n        await super.load(...arguments);\n        let list = this.root;\n        let hasRecords;\n        const isGrouped = list.isGrouped;\n        if (!isGrouped) {\n            hasRecords = list.records.length;\n        } else {\n            hasRecords = list.groups.some((g) => g.list.records.length);\n        }\n        if (!hasRecords) {\n            if (isGrouped) {\n                const commonConfig = {\n                    resModel: list.config.resModel,\n                    fields: list.config.fields,\n                    activeFields: list.config.activeFields,\n                    groupByFieldName: list.groupByField.name,\n                    context: list.context,\n                    list: {\n                        resModel: list.config.resModel,\n                        fields: list.config.fields,\n                        activeFields: list.config.activeFields,\n                        groupBy: [],\n                        context: list.context,\n                    },\n                };\n\n                const data = {\n                    count: 0,\n                    length: 0,\n                    records: [],\n                    __domain: [],\n                    value: \"fake\",\n                    displayName: \"Fake Group\",\n                    groups: [\n                        {\n                            display_name: false,\n                            count: 0,\n                        },\n                    ],\n                };\n\n                list.config.groups.fake = commonConfig;\n\n                const group = list._createGroupDatapoint(data);\n                list.groups.push(group);\n                list = group.list;\n            }\n            await list.addNewRecord();\n        }\n    }\n}\n\nclass KanbanEditorController extends kanbanView.Controller {\n    setup() {\n        super.setup();\n        useModelConfigFetchInvisible(this.model);\n    }\n\n    get modelParams() {\n        const params = super.modelParams;\n        params.groupsLimit = 1;\n        return params;\n    }\n}\n\nfunction isValidKanbanHook({ hook, element }) {\n    const draggingStructure = element.dataset.structure;\n    return hook.dataset.structures.split(\",\").includes(draggingStructure);\n}\n\nasync function addKanbanViewStructure(structure) {\n    switch (structure) {\n        case \"aside\": {\n            if (!this.viewEditorModel.xmlDoc.querySelector(\"main\")) {\n                this.env.viewEditorModel.pushOperation({\n                    type: \"kanban_wrap_main\",\n                    wrap_type: \"aside\",\n                });\n            }\n            return {\n                node: {\n                    tag: \"aside\",\n                    attrs: {\n                        class: \"col-2\",\n                    },\n                },\n                target: {\n                    tag: \"main\",\n                },\n            };\n        }\n        case \"footer\": {\n            if (!this.viewEditorModel.xmlDoc.querySelector(\"main\")) {\n                this.env.viewEditorModel.pushOperation({\n                    type: \"kanban_wrap_main\",\n                    wrap_type: \"footer\",\n                });\n            }\n            return {\n                node: {\n                    tag: \"footer\",\n                },\n                target: {\n                    tag: \"main\",\n                },\n                position: \"inside\",\n            };\n        }\n        case \"t\": {\n            return { type: \"kanban_menu\" };\n        }\n        case \"ribbon\": {\n            const cardTemplate = this.viewEditorModel.xmlDoc.querySelector(`[t-name=\"${KANBAN_CARD_ATTRIBUTE}\"]`);\n            let ribbonTarget;\n            if (cardTemplate.children.length) {\n                ribbonTarget = [`//kanban//t[@t-name=\"${KANBAN_CARD_ATTRIBUTE}\"]/*[1]`, \"before\"];\n            } else {\n                ribbonTarget = [`//kanban//t[@t-name=\"${KANBAN_CARD_ATTRIBUTE}\"]`, \"inside\"];\n            }\n            return {\n                node: {\n                    tag: \"widget\",\n                    attrs: {\n                        name: \"web_ribbon\",\n                        title: \"Demo\",\n                    },\n                },\n\n                target: this.env.viewEditorModel.getFullTarget(\n                    ribbonTarget[0],\n                    { isXpathFullAbsolute: false }\n                ),\n                position: ribbonTarget[1],\n            };\n        }\n        case \"kanban_colorpicker\": {\n            if (!this.viewEditorModel.xmlDoc.querySelector(\"t[t-name=menu]\")) {\n                this.env.viewEditorModel.pushOperation({\n                    type: \"kanban_menu\",\n                });\n            }\n            return {\n                type: \"kanban_colorpicker\",\n                view_id: this.env.viewEditorModel.mainView.id,\n            };\n        }\n    }\n}\n\nfunction prepareForKanbanDrag({ element, ref }) {\n    const hooksToStylize = [...ref.el.querySelectorAll(\".o_web_studio_hook\")].filter((e) =>\n        e.dataset.structures?.split(\",\").includes(element.dataset.structure)\n    );\n    hooksToStylize.forEach((e) => e.classList.add(\"o_web_studio_hook_visible\"));\n    return () => {\n        ref.el\n            .querySelectorAll(\".o_web_studio_hook_visible\")\n            .forEach((el) => el.classList.remove(\"o_web_studio_hook_visible\"));\n    };\n}\n\nconst kanbanEditor = {\n    ...kanbanView,\n    Compiler: KanbanEditorCompiler,\n    Controller: KanbanEditorController,\n    ArchParser: EditorArchParser,\n    Renderer: KanbanEditorRenderer,\n    Record: KanbanEditorRecord,\n    Model: OneRecordModel,\n    Sidebar: KanbanEditorSidebar,\n    isValidHook: isValidKanbanHook,\n    addViewStructure: addKanbanViewStructure,\n    prepareForDrag: prepareForKanbanDrag,\n    props(genericProps, editor, config) {\n        const props = kanbanView.props(genericProps, editor, config);\n        props.defaultGroupBy = props.archInfo.defaultGroupBy;\n        props.Model = makeModelErrorResilient(OneRecordModel);\n        props.limit = 1;\n        props.Renderer = KanbanEditorRenderer;\n        return props;\n    },\n};\n\nregistry.category(\"studio_editors\").add(\"kanban\", kanbanEditor);\n", "import { KanbanCompiler } from \"@web/views/kanban/kanban_compiler\";\nimport { computeXpath, applyInvisible } from \"../xml_utils\";\nimport { createElement, getTag } from \"@web/core/utils/xml\";\nimport { isComponentNode } from \"@web/views/view_compiler\";\n\nconst interestingSelector = [\n    \"div\",\n    \"aside\",\n    \"footer\",\n    \"field\",\n    \"main\",\n    \"kanban\",\n    \"widget\",\n    \"a\",\n    \"button\",\n].join(\", \");\n\n/**\n * @param {Element} el\n * @returns {string}\n */\nfunction getElementXpath(el) {\n    const xpath = el.getAttribute(\"studioXpath\");\n    if (isComponentNode(el)) {\n        return xpath;\n    }\n    return `\"${xpath}\"`;\n}\n\n/**\n * @param {Element} el\n * @param {string} xpath\n */\nfunction setElementXpath(el, xpath) {\n    if (isComponentNode(el)) {\n        el.setAttribute(\"studioXpath\", `'${xpath}'`);\n    } else {\n        el.setAttribute(\"studioXpath\", xpath);\n    }\n}\n\nexport class KanbanEditorCompiler extends KanbanCompiler {\n    applyInvisible(invisible, compiled, params) {\n        return applyInvisible(invisible, compiled, params);\n    }\n\n    /**\n     * Wrap the given node with a <t> element containing hooks before and after it.\n     * @param {Element} node - The node to wrap with hooks\n     * @param {string} type - The type of the hook\n     * @param {string} template - The template to use for the hook\n     * @param {boolean} insertBefore - Whether to insert the before hook\n     * @returns {Element} The wrapped node\n     */\n    addStudioHook(node, type, template, { structures, wrap = false } = {}) {\n        const xpath = getElementXpath(node);\n        if (wrap) {\n            const studioHookBefore = createElement(\"StudioHook\", {\n                xpath,\n                position: \"'before'\",\n                type: `'${type}'`,\n                subTemplate: `'${template}'`,\n                structures,\n            });\n            node.insertAdjacentElement(\"beforebegin\", studioHookBefore);\n        }\n        const studioHookAfter = createElement(\"StudioHook\", {\n            xpath,\n            position: \"'after'\",\n            type: `'${type}'`,\n            subTemplate: `'${template}'`,\n            structures,\n        });\n        node.insertAdjacentElement(\"afterend\", studioHookAfter);\n    }\n\n    wrapNodesInMain(node) {\n        const elementsToWrap = Array.from(node.children).filter((e) => {\n            if (e.tagName === \"widget\") {\n                return e.getAttribute(\"name\") !== \"web_ribbon\";\n            }\n            if (e.tagName === \"t\" && e.getAttribute(\"t-name\") === \"menu\") {\n                return false;\n            }\n            return ![\"aside\"].includes(e.tagName);\n        });\n        return createElement(\"main\", elementsToWrap);\n    }\n\n    compile(key, params = {}) {\n        const xml = this.templates[key];\n        const interestingArchNodes = [...xml.querySelectorAll(interestingSelector)];\n        for (const el of interestingArchNodes) {\n            const xpath = computeXpath(el, \"kanban\");\n            setElementXpath(el, xpath);\n        }\n        const compiled = super.compile(key, params);\n        return compiled;\n    }\n\n    compileButton(el, params) {\n        const compiled = super.compileButton(...arguments);\n        setElementXpath(compiled, el.getAttribute(\"studioXpath\"));\n        return compiled;\n    }\n\n    compileCard(node, params) {\n        const mainNode = node.querySelector(\"main\");\n        if (!mainNode) {\n            // to ease the addition of studio hooks in the UI, we make sure the kanban card contains a <main> node,\n            // which wraps the content of the card, even if the original template didn't compile this node\n            const mainEl = this.wrapNodesInMain(node);\n            setElementXpath(mainEl, \"kanban\");\n            node.append(mainEl);\n        }\n        const asideNode = node.querySelector(\"aside\");\n        const ribbonNode = node.querySelector(\"widget[name='web_ribbon']\");\n        const compiledCard = super.compileNode(node, params);\n        const compiledMain = compiledCard.querySelector(\"main\");\n        if (!ribbonNode) {\n            this.addStudioHook(compiledMain, \"ribbon\", \"kanbanRibbon\");\n        }\n        if (!asideNode) {\n            this.addStudioHook(compiledMain, \"kanbanAsideHook\", \"kanbanAsideHook\", { wrap: true });\n        }\n        return compiledCard;\n    }\n\n    compileField(el, params) {\n        const compiled = super.compileField(...arguments);\n        if (!el.hasAttribute(\"widget\")) {\n            compiled.classList.add(\"o-web-studio-editor--element-clickable\");\n\n            // Set empty class\n            const fieldName = el.getAttribute(\"name\");\n            const recordValueExpr = `__comp__.props.record.data[\"${fieldName}\"]`;\n            const isEmptyExpr = `__comp__.isFieldValueEmpty(${recordValueExpr})`;\n            compiled.setAttribute(\n                \"t-attf-class\",\n                `{{ ${isEmptyExpr} ? \"o_web_studio_widget_empty\" : \"\" }}`\n            );\n            const fieldNameExpr = `__comp__.props.record.fields[\"${fieldName}\"].string`;\n            const originalTOut = compiled.getAttribute(\"t-out\");\n            compiled.setAttribute(\"t-out\", `${isEmptyExpr} ? ${fieldNameExpr} : ${originalTOut}`);\n        } else {\n            compiled.setAttribute(\"hasEmptyPlaceholder\", true);\n        }\n        return compiled;\n    }\n\n    compileInnerSection(compiled) {\n        const interestingNodes = [...compiled.querySelectorAll(\"[studioXpath]\")].filter(\n            (e) => e.getAttribute(\"studioXpath\") !== \"null\" && !e.closest(\"ViewButton\")\n        );\n        const otherNodes = [...compiled.querySelectorAll(\"div[studioXpath]\")].filter(\n            (e) => !e.getAttribute(\"t-out\")\n        );\n        for (const child of otherNodes) {\n            // add a visual indication around structuring elements of the main element\n            child.classList.add(\"o_inner_section\");\n            child.classList.add(\"o-web-studio-editor--element-clickable\");\n        }\n        for (const child of interestingNodes) {\n            if (\n                [...child.getAttributeNames()].filter((e) =>\n                    [\"t-if\", \"t-elif\", \"t-else\"].includes(e)\n                ).length\n            ) {\n                // Don't append a studio hook if a condition is on the tag itself\n                // otherwise it may cause inconsistencies in the arch itself\n                // ie `<field t-elif=\"someConditon\" /><field name=\"newField\" /><t t-else=\"\"/>` would be invalid\n                continue;\n            }\n            this.addStudioHook(\n                child,\n                \"field\",\n                // for inline display, the studio hook must be a span to keep the current look\n                child.tagName === \"span\" ? \"kanbanInline\" : \"defaultTemplate\",\n                { structures: `'field'`, wrap: true }\n            );\n        }\n        if (!interestingNodes.length) {\n            const hook = createElement(\"StudioHook\", {\n                xpath: `\"${compiled.getAttribute(\"studioXpath\")}\"`,\n                position: \"'inside'\",\n                type: \"'field'\",\n                subTemplate: \"defaultTemplate\",\n                structures: `'field'`,\n            });\n            compiled.appendChild(hook);\n        }\n    }\n\n    compileNode(node, params) {\n        let compiled;\n        if (node.getAttribute?.(\"t-name\") === \"card\") {\n            compiled = this.compileCard(node);\n        } else {\n            compiled = super.compileNode(node, { ...params, compileInvisibleNodes: true });\n        }\n        if ([\"aside\", \"footer\"].includes(getTag(node))) {\n            compiled.classList.add(\"o_inner_section\");\n            compiled.classList.add(\"o-web-studio-editor--element-clickable\");\n            this.compileInnerSection(compiled);\n        } else if (getTag(node) === \"main\") {\n            this.compileInnerSection(compiled);\n            if (!compiled.querySelector(\"footer\")) {\n                const footerHook = createElement(\"StudioHook\", {\n                    xpath: `\"${compiled.getAttribute(\"studioXpath\")}\"`,\n                    position: \"'after'\",\n                    type: `'footer'`,\n                    subTemplate: `'defaultTemplate'`,\n                    structures: `'footer'`,\n                });\n                compiled.appendChild(footerHook);\n            }\n        }\n        // Propagate the xpath to the compiled template for most nodes.\n        if (node.nodeType === 1 && compiled && !compiled.attributes.studioXpath) {\n            setElementXpath(compiled, node.getAttribute(\"studioXpath\"));\n        }\n        return compiled;\n    }\n}\n", "import { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { FieldStudio } from \"@web_studio/client_action/view_editor/editors/components/field_studio\";\nimport { WidgetStudio } from \"@web_studio/client_action/view_editor/editors/components/widget_studio\";\nimport { computeXpath } from \"@web_studio/client_action/view_editor/editors/xml_utils\";\nimport { ViewButtonStudio } from \"@web_studio/client_action/view_editor/editors/components/view_button_studio\";\nimport { StudioHook } from \"@web_studio/client_action/view_editor/editors/components/studio_hook_component\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, toRaw, useEnv, useState, xml, onError } from \"@odoo/owl\";\nimport { KanbanEditorCompiler } from \"./kanban_editor_compiler\";\n\nclass FieldStudioKanbanRecord extends FieldStudio {\n    isX2ManyEditable() {\n        return false;\n    }\n}\n\nconst KanbanRecord = kanbanView.Renderer.components.KanbanRecord;\n\nclass KanbanEditorRecordMenu extends Component {\n    static props = {\n        slots: Object,\n        studioXpath: String,\n    };\n    static template = xml`\n        <div class=\"o_dropdown_kanban bg-transparent position-absolute end-0 top-0 o-web-studio-editor--element-clickable\" t-att-studioXpath=\"props.studioXpath\">\n            <button class=\"btn o-no-caret rounded-0 pe-none\" title=\"Dropdown menu\">\n                <span class=\"fa fa-ellipsis-v\"/>\n            </button>\n        </div>\n    `;\n}\n\nfunction useSafeKanban() {\n    const state = useState({ hasError: false });\n    const viewEditorModel = toRaw(useEnv().viewEditorModel);\n    onError((error) => {\n        const hasError = state.hasError;\n        if (hasError || viewEditorModel.isInEdition) {\n            throw error;\n        }\n        state.hasError = true;\n    });\n    return state;\n}\n\nclass SafeKanbanRecord extends KanbanRecord {\n    static template = \"web_studio.SafeKanbanRecord\";\n    setup() {\n        super.setup();\n        this.safe = useSafeKanban();\n    }\n    onGlobalClick() {\n        // prevent click handling by the component\n    }\n}\n\nclass _KanbanEditorRecord extends KanbanRecord {\n    static template = \"web_studio.SafeKanbanRecord\";\n    static menuTemplate = \"web_studio.SafeKanbanRecordMenu\";\n    static components = {\n        ...KanbanRecord.components,\n        Field: FieldStudioKanbanRecord,\n        Widget: WidgetStudio,\n        StudioHook,\n        ViewButton: ViewButtonStudio,\n        KanbanRecordMenu: KanbanEditorRecordMenu,\n    };\n    setup() {\n        super.setup();\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.dialogService = useService(\"dialog\");\n        this.safe = useSafeKanban();\n    }\n    onGlobalClick(ev) {\n        const el = ev.target.closest(\".o-web-studio-editor--element-clickable\");\n        if (el) {\n            this.env.config.onNodeClicked(el.getAttribute(\"studioxpath\"));\n        }\n    }\n    isFieldValueEmpty(value) {\n        if (value === null) {\n            return true;\n        }\n        if (Array.isArray(value)) {\n            return !value.length;\n        }\n        return !value;\n    }\n    get dropdownXpath() {\n        const compiledTemplateMenu = this.props.templates[this.constructor.KANBAN_MENU_ATTRIBUTE];\n        return computeXpath(compiledTemplateMenu, \"kanban\");\n    }\n}\n\nexport class KanbanEditorRecord extends Component {\n    static props = [...KanbanRecord.props];\n    static template = xml`<t t-component=\"KanbanRecord\" t-props=\"kanbanRecordProps\" />`;\n\n    get KanbanRecord() {\n        if (this.env.viewEditorModel.mode !== \"interactive\") {\n            return SafeKanbanRecord;\n        } else {\n            return _KanbanEditorRecord;\n        }\n    }\n    get kanbanRecordProps() {\n        const props = { ...this.props };\n        if (this.env.viewEditorModel.mode === \"interactive\") {\n            props.Compiler = KanbanEditorCompiler;\n        }\n        return props;\n    }\n}\n", "import { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanHeader } from \"@web/views/kanban/kanban_header\";\nimport { KanbanEditorRecord } from \"@web_studio/client_action/view_editor/editors/kanban/kanban_editor_record\";\nimport { useRef, useEffect } from \"@odoo/owl\";\n\nclass KanbanEditorHeader extends KanbanHeader {\n    static template = \"web_studio.KanbanEditorHeader\";\n}\n\nexport class KanbanEditorRenderer extends kanbanView.Renderer {\n    static template = \"web_studio.KanbanEditorRenderer\";\n    static components = {\n        ...kanbanView.Renderer.components,\n        KanbanRecord: KanbanEditorRecord,\n        KanbanHeader: KanbanEditorHeader,\n    };\n\n    setup() {\n        super.setup();\n        const rootRef = useRef(\"root\");\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n                el.classList.add(\"o_web_studio_kanban_view_editor\");\n            },\n            () => [rootRef.el]\n        );\n    }\n\n    get canUseSortable() {\n        return false;\n    }\n\n    get showNoContentHelper() {\n        return false;\n    }\n\n    getGroupsOrRecords() {\n        const { list } = this.props;\n        const groupsOrRec = super.getGroupsOrRecords(...arguments);\n        if (list.isGrouped) {\n            return [groupsOrRec.filter((el) => el.group.list.records.length)[0]];\n        } else {\n            return [groupsOrRec[0]];\n        }\n    }\n\n    canCreateGroup() {\n        return false;\n    }\n\n    getGroupUnloadedCount() {\n        return 0;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ViewStructures } from \"@web_studio/client_action/view_editor/editors/components/view_structures\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { ExistingFields } from \"@web_studio/client_action/view_editor/editors/components/view_fields\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { Properties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/properties\";\nimport { KanbanButtonProperties } from \"@web_studio/client_action/view_editor/editors/kanban/kanban_editor_sidebar/properties/kanban_button_properties/kanban_button_properties\";\nimport { FieldProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/field_properties/field_properties\";\nimport { WidgetProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/widget_properties/widget_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { AsideProperties } from \"./properties/aside_properties/aside_properties\";\nimport { FooterProperties } from \"./properties/footer_properties/footer_properties\";\nimport { MenuProperties } from \"./properties/menu_properties/menu_properties\";\nimport { DivProperties } from \"./properties/div_properties/div_properties\";\n\nclass KanbanFieldProperties extends FieldProperties {\n    onChangeAttribute(value, name) {\n        if (name === \"bold\") {\n            let cls = this.props.node.attrs.class;\n            if (value) {\n                cls = cls ? `fw-bold ${cls}` : \"fw-bold\";\n            } else {\n                cls = cls\n                    .split(\" \")\n                    .filter((c) => c !== \"fw-bold\" && c !== \"fw-bolder\")\n                    .join(\" \");\n            }\n            return this.editNodeAttributes({ class: cls });\n        }\n        return super.onChangeAttribute(...arguments);\n    }\n}\n\nexport class KanbanEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.KanbanEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        ExistingFields,\n        Property,\n        Properties,\n        ViewStructures,\n        SidebarViewToolbox,\n    };\n    static get viewStructures() {\n        return {\n            aside: {\n                name: _t(\"Side panel\"),\n                class: \"o_web_studio_field_aside\",\n                isVisible: (vem) => !vem.controllerProps.arch.querySelector(\"aside\"),\n            },\n            footer: {\n                name: _t(\"Footer\"),\n                class: \"o_web_studio_field_footer\",\n                isVisible: (vem) => !vem.controllerProps.arch.querySelector(\"footer\"),\n            },\n            t: {\n                name: _t(\"Menu\"),\n                class: \"o_web_studio_field_menu\",\n                isVisible: (vem) => !vem.controllerProps.arch.querySelector(\"t[t-name=menu]\"),\n            },\n            ribbon: {\n                name: _t(\"Ribbon\"),\n                class: \"o_web_studio_field_ribbon\",\n                isVisible: (vem) =>\n                    !vem.controllerProps.arch.querySelector(\"widget[name=web_ribbon]\"),\n            },\n            kanban_colorpicker: {\n                name: _t(\"Color Picker\"),\n                class: \"o_web_studio_field_color_picker\",\n                isVisible: (vem) =>\n                    !vem.controllerProps.arch.querySelector(\"field[widget=kanban_color_picker]\"),\n            },\n        };\n    }\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n        this.propertiesComponents = {\n            a: {\n                component: KanbanButtonProperties,\n            },\n            button: {\n                component: KanbanButtonProperties,\n            },\n            field: {\n                component: KanbanFieldProperties,\n                props: {\n                    availableOptions: [\"invisible\", \"string\", \"bold\"],\n                },\n            },\n            aside: {\n                component: AsideProperties,\n            },\n            div: {\n                component: DivProperties,\n            },\n            footer: {\n                component: FooterProperties,\n            },\n            t: {\n                component: MenuProperties,\n            },\n            widget: {\n                component: WidgetProperties,\n            },\n        };\n    }\n\n    get archInfo() {\n        return this.viewEditorModel.controllerProps.archInfo;\n    }\n\n    get colorField() {\n        return {\n            choices: fieldsToChoices(this.viewEditorModel.fields, [\"integer\"]),\n            required: false,\n        };\n    }\n\n    get defaultGroupBy() {\n        return {\n            choices: fieldsToChoices(\n                this.viewEditorModel.fields,\n                this.viewEditorModel.GROUPABLE_TYPES,\n                (field) => field.store\n            ),\n            required: false,\n        };\n    }\n\n    get defaultOrder() {\n        if (this.archInfo.defaultOrder.length >= 1) {\n            return this.archInfo.defaultOrder[0];\n        } else {\n            return { name: \"\", asc: true };\n        }\n    }\n\n    get sortChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) => field.store\n        );\n    }\n\n    get orderChoices() {\n        return [\n            { value: \"asc\", label: _t(\"Ascending\") },\n            { value: \"desc\", label: _t(\"Descending\") },\n        ];\n    }\n\n    setSortBy(value) {\n        this.onSortingChanged(value, this.defaultOrder.asc ? \"asc\" : \"desc\");\n    }\n\n    setOrder(value) {\n        this.onSortingChanged(this.defaultOrder.name, value);\n    }\n\n    onSortingChanged(sortBy, order) {\n        if (sortBy) {\n            this.editAttribute(`${sortBy} ${order}`, \"default_order\");\n        } else {\n            this.editAttribute(\"\", \"default_order\");\n        }\n    }\n\n    editAttribute(value, name) {\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    editDefaultGroupBy(value) {\n        this.editAttribute(value || \"\", \"default_group_by\");\n    }\n\n    editColor(value) {\n        if (value && !this.viewEditorModel.fieldsInArch.includes(value)) {\n            this.viewEditorModel.doOperation({\n                type: \"add\",\n                target: {\n                    tag: \"kanban\",\n                },\n                position: \"inside\",\n                node: {\n                    attrs: {\n                        name: value,\n                        invisible: \"1\",\n                    },\n                    tag: \"field\",\n                },\n            });\n        }\n        this.editAttribute(value || \"\", \"highlight_color\");\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { ClassAttribute } from \"@web_studio/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class AsideProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Aside\";\n    static components = {\n        ClassAttribute,\n        Property,\n        ViewStructureProperties,\n    };\n    static props = {\n        node: { type: Object },\n    };\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { ClassAttribute } from \"@web_studio/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { ModifiersProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/modifiers/modifiers_properties\";\n\nexport class DivProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Div\";\n    static components = {\n        ClassAttribute,\n        ModifiersProperties,\n        Property,\n        ViewStructureProperties,\n    };\n    static props = {\n        node: { type: Object },\n    };\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { ClassAttribute } from \"@web_studio/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class FooterProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Footer\";\n    static components = {\n        ClassAttribute,\n        Property,\n        ViewStructureProperties,\n    };\n    static props = {\n        node: { type: Object },\n    };\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { ClassAttribute } from \"@web_studio/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute\";\nimport { LimitGroupVisibility } from \"@web_studio/client_action/view_editor/interactive_editor/properties/limit_group_visibility/limit_group_visibility\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\nimport { ModifiersProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/modifiers/modifiers_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class KanbanButtonProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.KanbanButton\";\n    static props = {\n        node: { type: Object },\n    };\n    static components = {\n        ClassAttribute,\n        LimitGroupVisibility,\n        ModifiersProperties,\n        Property,\n        SidebarPropertiesToolbox,\n    };\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n        // We don't want to display a in the sidebar.\n        this.env.viewEditorModel.activeNode.humanName = _t(\"Button\");\n    }\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\n\nexport class MenuProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Menu\";\n    static components = {\n        Property,\n        SidebarPropertiesToolbox,\n    };\n    static props = {\n        node: { type: Object },\n    };\n    get colorPicker() {\n        return this.env.viewEditorModel.controllerProps.arch.querySelector(\n            \"field[widget=kanban_color_picker]\"\n        );\n    }\n    editColorPicker() {\n        this.env.viewEditorModel.activeNodeXpath = this.colorPicker.getAttribute(\"studioXpath\");\n    }\n}\n", "/** @odoo-module */\nimport { LEGACY_KANBAN_MENU_ATTRIBUTE } from \"@web/views/kanban/kanban_arch_parser\";\nimport { KanbanCompiler } from \"@web/views/kanban/kanban_compiler\";\nimport { computeXpath, applyInvisible } from \"../xml_utils\";\nimport { isComponentNode } from \"@web/views/view_compiler\";\nimport { createElement } from \"@web/core/utils/xml\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst interestingSelector = [\n    \"field\",\n    \"widget\",\n    \".dropdown\",\n    \"img.oe_kanban_avatar\",\n    \".o_kanban_record_body\",\n    \".o_kanban_record_bottom\",\n].join(\", \");\n\nconst hookBaseClass = \"o_web_studio_kanban_hook cursor-pointer text-primary fw-bolder \";\n\nexport class KanbanEditorCompilerLegacy extends KanbanCompiler {\n    constructor() {\n        super(...arguments);\n        const kanbanBox = this.templates[\"kanban-box\"];\n        this.isDashboard = kanbanBox.closest(\"kanban\").classList.contains(\"o_kanban_dashboard\");\n    }\n\n    applyInvisible(invisible, compiled, params) {\n        return applyInvisible(invisible, compiled, params);\n    }\n\n    compile(key, params = {}) {\n        const xml = this.templates[key];\n\n        // One pass to compute and add the xpath for the arch's node location\n        // onto that node.\n        const mainDiv = xml.querySelector(\"div\");\n        const interestingArchNodes = [...xml.querySelectorAll(interestingSelector)];\n        if (mainDiv) {\n            interestingArchNodes.push(mainDiv);\n        }\n        for (const el of interestingArchNodes) {\n            const xpath = computeXpath(el, \"kanban\");\n            el.setAttribute(\"studioXpath\", xpath);\n        }\n\n        const compiled = super.compile(key, params);\n\n        const isKanbanBox = key === \"kanban-box\";\n\n        if (isKanbanBox && !this.isDashboard && mainDiv) {\n            const tagsWidget = xml.querySelector(\"field[widget='many2many_tags']\");\n            if (!tagsWidget) {\n                this.addTagsWidgetHook(compiled);\n            }\n\n            const priorityWidget = xml.querySelector(\"field[widget='priority']\");\n            const favoriteWidget = xml.querySelector(\"field[widget='boolean_favorite']\");\n            if (!priorityWidget && !favoriteWidget) {\n                this.addPriorityHook(compiled);\n            }\n\n            const dropdown = this.templates[LEGACY_KANBAN_MENU_ATTRIBUTE];\n            if (!dropdown) {\n                this.addDropdownHook(compiled);\n            }\n\n            const avatarImg = xml.querySelector(\"img.oe_kanban_avatar\");\n            if (!avatarImg) {\n                this.addAvatarHook(compiled);\n            }\n        }\n\n        compiled.querySelectorAll(\".oe_kanban_avatar\").forEach((el) => {\n            const tIf = el.closest(\"[t-if]\");\n            if (tIf) {\n                const tElse = createElement(\"t\", {\n                    \"t-else\": \"\",\n                    \"t-call\": \"web_studio.KanbanEditorRecord.AvatarPlaceholder\",\n                });\n                tIf.insertAdjacentElement(\"afterend\", tElse);\n            }\n        });\n\n        return compiled;\n    }\n\n    compileField(node) {\n        const compiled = super.compileField(...arguments);\n        if (compiled.tagName === \"span\") {\n            const fieldName = node.getAttribute(\"name\");\n            compiled.setAttribute(\"data-field-name\", fieldName);\n        } else {\n            compiled.setAttribute(\"hasEmptyPlaceholder\", true);\n        }\n\n        return compiled;\n    }\n\n    addStudioHook(node, compiled) {\n        const tNode = createElement(\"t\");\n        if (compiled.hasAttribute(\"t-if\")) {\n            // t-if from the invisible modifier\n            tNode.setAttribute(\"t-if\", compiled.getAttribute(\"t-if\"));\n            compiled.removeAttribute(\"t-if\");\n        }\n        tNode.appendChild(compiled);\n        const xpath = node.getAttribute(\"studioXpath\");\n        const studioHook = createElement(\"StudioHook\", {\n            xpath: `\"${xpath}\"`,\n            position: \"'after'\",\n        });\n        tNode.appendChild(studioHook);\n        return tNode;\n    }\n\n    compileNode(node, params) {\n        const nodeType = node.nodeType;\n        if (nodeType === 1 && (isComponentNode(node) || node.getAttribute(\"studio_no_fetch\"))) {\n            return;\n        }\n\n        let compiled = super.compileNode(node, { ...params, compileInvisibleNodes: true });\n\n        if (nodeType === 1 && compiled) {\n            // Put a xpath on anything of interest.\n            if (node.hasAttribute(\"studioXpath\")) {\n                const xpath = node.getAttribute(\"studioXpath\");\n                if (isComponentNode(compiled)) {\n                    compiled.setAttribute(\"studioXpath\", `\"${xpath}\"`);\n                } else if (!compiled.hasAttribute(\"studioXpath\")) {\n                    compiled.setAttribute(\"studioXpath\", xpath);\n                }\n\n                if (node.classList.contains(\"oe_kanban_avatar\")) {\n                    compiled.setAttribute(\n                        \"t-on-click\",\n                        `(ev) => __comp__.env.config.onNodeClicked(\"${xpath}\")`\n                    );\n                    compiled.classList.add(\"o-web-studio-editor--element-clickable\");\n                }\n                if (node.tagName === \"field\" && !isComponentNode(compiled)) {\n                    compiled.setAttribute(\n                        \"t-on-click\",\n                        `(ev) => __comp__.env.config.onNodeClicked(\"${xpath}\")`\n                    );\n                    compiled.classList.add(\"o-web-studio-editor--element-clickable\");\n\n                    const fieldName = node.getAttribute(\"name\");\n                    const isEmptyExpr = `__comp__.isFieldValueEmpty(record[\"${fieldName}\"].value)`;\n\n                    // Set empty class\n                    const tattfClassEmpty = `{{ ${isEmptyExpr} ? \"o_web_studio_widget_empty\" : \"\" }}`;\n\n                    const tattfClass = compiled.getAttribute(\"t-attf-class\");\n\n                    const nextAttfClass = tattfClass\n                        ? `${tattfClass} ${tattfClassEmpty}`\n                        : tattfClassEmpty;\n                    compiled.setAttribute(\"t-attf-class\", nextAttfClass);\n\n                    // Set field name on empty\n                    const fieldId = node.getAttribute(\"field_id\");\n                    const tOut = compiled.getAttribute(\"t-out\");\n                    compiled.setAttribute(\n                        \"t-out\",\n                        `${isEmptyExpr} ? __comp__.props.archInfo.fieldNodes['${fieldId}'].string : ${tOut}`\n                    );\n                }\n                if (node.tagName === \"field\" || node.tagName === \"widget\") {\n                    // Don't append a studio hook if a condition is on the tag itself\n                    // otherwise it may cause inconsistencies in the arch itself\n                    // ie `<field t-elif=\"someCondifiton\" /><field name=\"newField\" /><t t-else=\"\"/>` would be invalid\n                    if (\n                        !Array.from(node.getAttributeNames()).filter((att) =>\n                            [\"t-if\", \"t-elif\", \"t-else\"].includes(att)\n                        )[0]\n                    ) {\n                        compiled = this.addStudioHook(node, compiled);\n                    }\n                }\n            }\n        }\n        return compiled;\n    }\n\n    addTagsWidgetHook(compiled) {\n        const parentElement =\n            compiled.querySelector(\".o_kanban_record_body\") || compiled.querySelector(\"div\");\n        const tagsHook = createElement(\"span\", {\n            class: hookBaseClass + \"o_web_studio_add_kanban_tags\",\n            \"t-on-click\": `() => __comp__.onAddTagsWidget({\n                xpath: \"${parentElement.getAttribute(\"studioXpath\")}\"\n            })`,\n        });\n        tagsHook.textContent = _t(\"Add tags\");\n\n        if (parentElement.firstChild) {\n            parentElement.insertBefore(tagsHook, parentElement.firstChild);\n        } else {\n            parentElement.appendChild(tagsHook);\n        }\n    }\n\n    addDropdownHook(compiled) {\n        const rootSibling = compiled.querySelector(\"div\");\n        const dropdownHook = createElement(\n            \"div\",\n            [\n                createElement(\"a\", {\n                    class: \"btn fa fa-ellipsis-v\",\n                }),\n            ],\n            {\n                class:\n                    hookBaseClass +\n                    \"o_web_studio_add_dropdown o_dropdown_kanban dropdown position-absolute end-0\",\n                style: \"z-index: 1;\",\n                \"t-on-click\": \"() => __comp__.onAddDropdown()\",\n            }\n        );\n        rootSibling.insertAdjacentElement(\"afterend\", dropdownHook);\n    }\n\n    addPriorityHook(compiled) {\n        const parentElement = compiled.querySelector(\"div\");\n        const priorityHook = createElement(\"div\", {\n            class:\n                hookBaseClass +\n                \"o_web_studio_add_priority oe_kanban_bottom_left align-self-start flex-grow-0\",\n            style: \"z-index: 1;\",\n            \"t-on-click\": \"() => __comp__.onAddPriority()\",\n        });\n        priorityHook.textContent = _t(\"Add a priority\");\n        parentElement.appendChild(priorityHook);\n    }\n\n    addAvatarHook(compiled) {\n        const parentElement =\n            compiled.querySelector(\".o_kanban_record_bottom\") || compiled.querySelector(\"div\");\n        const avatarHook = createElement(\"div\", {\n            class: hookBaseClass + \"o_web_studio_add_kanban_image oe_kanban_bottom_right pe-auto\",\n            style: \"z-index: 1;\",\n            \"t-on-click\": \"() => __comp__.onAddAvatar()\",\n        });\n        avatarHook.textContent = _t(\"Add an avatar\");\n        parentElement.appendChild(avatarHook);\n    }\n\n    /**\n     * In v16, some views use forbidden owl directives (t-on) directly\n     * in the arch. In master, they will be removed. The validation is deactivated\n     * in the js_class used to render those archs, but as in studio we do not use\n     * the js_class, we have to disable the validation in the editor.\n     * @override\n     */\n    validateNode() {}\n}\n", "/** @odoo-module */\nimport { registry } from \"@web/core/registry\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanEditorRendererLegacy } from \"@web_studio/client_action/view_editor/editors/kanban_legacy/kanban_editor_renderer_legacy\";\nimport { makeModelErrorResilient } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { KanbanEditorSidebarLegacy } from \"./kanban_editor_sidebar_legacy/kanban_editor_sidebar_legacy\";\nimport { getStudioNoFetchFields, useModelConfigFetchInvisible } from \"../utils\";\n\nclass EditorArchParser extends kanbanView.ArchParser {\n    parse(arch, models, modelName) {\n        const parsed = super.parse(...arguments);\n        const noFetch = getStudioNoFetchFields(parsed.fieldNodes);\n        parsed.fieldNodes = omit(parsed.fieldNodes, ...noFetch.fieldNodes);\n        parsed.progressAttributes = false;\n        parsed.canOpenRecords = false;\n        return parsed;\n    }\n}\nclass OneRecordModel extends kanbanView.Model {\n    async load() {\n        this.progressAttributes = false;\n        await super.load(...arguments);\n        let list = this.root;\n        let hasRecords;\n        const isGrouped = list.isGrouped;\n        if (!isGrouped) {\n            hasRecords = list.records.length;\n        } else {\n            hasRecords = list.groups.some((g) => g.list.records.length);\n        }\n        if (!hasRecords) {\n            if (isGrouped) {\n                const commonConfig = {\n                    resModel: list.config.resModel,\n                    fields: list.config.fields,\n                    activeFields: list.config.activeFields,\n                    groupByFieldName: list.groupByField.name,\n                    context: list.context,\n                    list: {\n                        resModel: list.config.resModel,\n                        fields: list.config.fields,\n                        activeFields: list.config.activeFields,\n                        groupBy: [],\n                        context: list.context,\n                    },\n                };\n\n                const data = {\n                    count: 0,\n                    length: 0,\n                    records: [],\n                    __domain: [],\n                    value: \"fake\",\n                    displayName: \"fake\",\n                    groups: [\n                        {\n                            display_name: false,\n                            count: 0,\n                        },\n                    ],\n                };\n\n                list.config.groups.fake = commonConfig;\n\n                const group = list._createGroupDatapoint(data);\n                list.groups.push(group);\n                list = group.list;\n            }\n            await list.addNewRecord();\n        }\n    }\n}\n\nclass KanbanEditorControllerLegacy extends kanbanView.Controller {\n    setup() {\n        super.setup();\n        useModelConfigFetchInvisible(this.model);\n    }\n\n    get modelParams() {\n        const params = super.modelParams;\n        params.groupsLimit = 1;\n        return params;\n    }\n}\n\nconst kanbanEditor = {\n    ...kanbanView,\n    Controller: KanbanEditorControllerLegacy,\n    ArchParser: EditorArchParser,\n    Renderer: KanbanEditorRendererLegacy,\n    Model: OneRecordModel,\n    Sidebar: KanbanEditorSidebarLegacy,\n    props(genericProps, editor, config) {\n        const props = kanbanView.props(genericProps, editor, config);\n        props.defaultGroupBy = props.archInfo.defaultGroupBy;\n        props.Model = makeModelErrorResilient(OneRecordModel);\n        props.limit = 1;\n        props.Renderer = KanbanEditorRendererLegacy;\n        return props;\n    },\n};\nregistry.category(\"studio_editors\").add(\"kanban_legacy\", kanbanEditor);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\n\nimport { KanbanEditorCompilerLegacy } from \"@web_studio/client_action/view_editor/editors/kanban_legacy/kanban_editor_compiler_legacy\";\nimport { FieldStudio } from \"@web_studio/client_action/view_editor/editors/components/field_studio\";\nimport { WidgetStudio } from \"@web_studio/client_action/view_editor/editors/components/widget_studio\";\nimport { ViewButtonStudio } from \"@web_studio/client_action/view_editor/editors/components/view_button_studio\";\nimport { StudioHook } from \"@web_studio/client_action/view_editor/editors/components/studio_hook_component\";\nimport { FieldSelectorDialog } from \"@web_studio/client_action/view_editor/editors/components/field_selector_dialog\";\n\nimport { computeXpath } from \"@web_studio/client_action/view_editor/editors/xml_utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AlertDialog, ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { Component, toRaw, useEnv, useState, xml, useEffect, useRef, onError } from \"@odoo/owl\";\n\nclass FieldStudioKanbanRecord extends FieldStudio {\n    isX2ManyEditable() {\n        return false;\n    }\n}\n\nconst OriginDropdown = kanbanView.Renderer.components.KanbanRecord.components.Dropdown;\nclass Dropdown extends OriginDropdown {\n    static template = \"web_studio.KanbanEditorRecord.Dropdown\";\n    static props = {\n        ...OriginDropdown.props,\n        studioXpath: { type: String, optional: 1 },\n        hasCoverSetter: { type: Boolean, optional: 1 },\n    };\n\n    setup() {\n        super.setup();\n        const rootRef = useRef(\"root\");\n        useEffect(\n            (rootEl) => {\n                if (this.props.studioXpath) {\n                    rootEl.classList.add(\"o-web-studio-editor--element-clickable\");\n                    rootEl.dataset.studioXpath = this.props.studioXpath;\n                }\n\n                if (this.props.hasCoverSetter) {\n                    rootEl.dataset.hasCoverSetter = true;\n                }\n            },\n            () => [rootRef.el]\n        );\n    }\n\n    handleClick() {\n        this.env.config.onNodeClicked(this.props.studioXpath);\n    }\n}\n\nconst KanbanRecord = kanbanView.Renderer.components.KanbanRecord;\n\nfunction useSafeKanban() {\n    const state = useState({ hasError: false });\n    const viewEditorModel = toRaw(useEnv().viewEditorModel);\n    onError((error) => {\n        const hasError = state.hasError;\n        if (hasError || viewEditorModel.isInEdition) {\n            throw error;\n        }\n        state.hasError = true;\n    });\n    return state;\n}\n\nclass SafeKanbanRecordLegacy extends KanbanRecord {\n    static template = \"web_studio.SafeKanbanRecordLegacy\";\n    setup() {\n        super.setup();\n        this.safe = useSafeKanban();\n    }\n}\n\nclass _KanbanEditorRecord extends KanbanRecord {\n    static template = \"web_studio.SafeKanbanRecordLegacy\";\n    static menuTemplate = \"web_studio.SafeKanbanRecordLegacyMenu\";\n    static components = {\n        ...KanbanRecord.components,\n        Dropdown,\n        Field: FieldStudioKanbanRecord,\n        Widget: WidgetStudio,\n        StudioHook,\n        ViewButton: ViewButtonStudio,\n    };\n    setup() {\n        super.setup();\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        if (this.constructor.LEGACY_KANBAN_MENU_ATTRIBUTE in this.props.templates) {\n            const compiledTemplateMenu =\n                this.props.templates[this.constructor.LEGACY_KANBAN_MENU_ATTRIBUTE];\n            this.dropdownXpath = computeXpath(compiledTemplateMenu, \"kanban\");\n            this.dropdownHasCoverSetter = Boolean(\n                compiledTemplateMenu.querySelectorAll(\"a[data-type='set_cover']\").length\n            );\n        }\n        this.dialogService = useService(\"dialog\");\n\n        this.safe = useSafeKanban();\n    }\n\n    onGlobalClick() {}\n\n    isFieldValueEmpty(value) {\n        if (value === null) {\n            return true;\n        }\n        if (Array.isArray(value)) {\n            return !value.length;\n        }\n        return !value;\n    }\n\n    onAddTagsWidget({ xpath }) {\n        const fields = [];\n        for (const [fName, field] of Object.entries(this.props.record.fields)) {\n            if (field.type === \"many2many\") {\n                const _field = { ...field, name: fName };\n                fields.push(_field);\n            }\n        }\n\n        if (!fields.length) {\n            this.dialogService.add(AlertDialog, {\n                body: _t(\"You first need to create a many2many field in the form view.\"),\n            });\n            return;\n        }\n\n        this.dialogService.add(FieldSelectorDialog, {\n            fields,\n            onConfirm: (field) => {\n                const operation = {\n                    type: \"add\",\n                    node: {\n                        tag: \"field\",\n                        attrs: { name: field },\n                    },\n                    target: this.env.viewEditorModel.getFullTarget(xpath),\n                    position: \"inside\",\n                };\n                this.env.viewEditorModel.doOperation(operation);\n            },\n        });\n    }\n\n    onAddDropdown() {\n        this.dialogService.add(ConfirmationDialog, {\n            body: _t(\"Do you want to add a dropdown with colors?\"),\n            confirm: () => {\n                this.env.viewEditorModel.doOperation({\n                    type: \"kanban_dropdown\",\n                });\n            },\n        });\n    }\n\n    onAddPriority() {\n        const fields = [];\n        const activeFields = Object.keys(this.props.record.activeFields);\n        for (const [fName, field] of Object.entries(this.props.record.fields)) {\n            if (field.type === \"selection\" && !activeFields.includes(fName)) {\n                const _field = { ...field, name: fName };\n                fields.push(_field);\n            }\n        }\n        this.dialogService.add(FieldSelectorDialog, {\n            fields,\n            showNew: true,\n            onConfirm: (field) => {\n                this.env.viewEditorModel.doOperation({\n                    type: \"kanban_priority\",\n                    field,\n                });\n            },\n        });\n    }\n\n    onAddAvatar() {\n        const fields = [];\n        for (const [fName, field] of Object.entries(this.props.record.fields)) {\n            if (\n                field.type === \"many2one\" &&\n                (field.relation === \"res.partner\" || field.relation === \"res.users\")\n            ) {\n                const _field = { ...field, name: fName };\n                fields.push(_field);\n            }\n        }\n        this.dialogService.add(FieldSelectorDialog, {\n            fields,\n            onConfirm: (field) => {\n                this.env.viewEditorModel.doOperation({\n                    type: \"kanban_image\",\n                    field,\n                });\n            },\n        });\n    }\n}\n\nexport class KanbanEditorRecordLegacy extends Component {\n    static props = [...KanbanRecord.props];\n    static template = xml`<t t-component=\"KanbanRecord\" t-props=\"kanbanRecordProps\" />`;\n\n    get KanbanRecord() {\n        if (this.env.viewEditorModel.mode !== \"interactive\") {\n            return SafeKanbanRecordLegacy;\n        } else {\n            return _KanbanEditorRecord;\n        }\n    }\n    get kanbanRecordProps() {\n        const props = { ...this.props };\n        if (this.env.viewEditorModel.mode === \"interactive\") {\n            props.Compiler = KanbanEditorCompilerLegacy;\n        }\n        return props;\n    }\n}\n", "/** @odoo-module */\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanEditorRecordLegacy } from \"@web_studio/client_action/view_editor/editors/kanban_legacy/kanban_editor_record_legacy\";\nimport { useRef, useEffect } from \"@odoo/owl\";\n\nexport class KanbanEditorRendererLegacy extends kanbanView.Renderer {\n    static template = \"web_studio.KanbanEditorRendererLegacy\";\n    static components = {\n        ...kanbanView.Renderer.components,\n        KanbanRecord: KanbanEditorRecordLegacy,\n    };\n\n    setup() {\n        super.setup();\n        const rootRef = useRef(\"root\");\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n                el.classList.add(\"o_web_studio_kanban_view_editor\");\n                el.classList.add(\"o_web_studio_kanban_view_editor_legacy\");\n            },\n            () => [rootRef.el]\n        );\n    }\n\n    get canUseSortable() {\n        return false;\n    }\n\n    get showNoContentHelper() {\n        return false;\n    }\n\n    getGroupsOrRecords() {\n        const { list } = this.props;\n        const groupsOrRec = super.getGroupsOrRecords(...arguments);\n        if (list.isGrouped) {\n            return [groupsOrRec.filter((el) => el.group.list.records.length)[0]];\n        } else {\n            return [groupsOrRec[0]];\n        }\n    }\n\n    canCreateGroup() {\n        return false;\n    }\n\n    getGroupUnloadedCount() {\n        return 0;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { ExistingFields } from \"@web_studio/client_action/view_editor/editors/components/view_fields\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { Properties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/properties\";\nimport { FieldProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/field_properties/field_properties\";\nimport { KanbanCoverProperties } from \"@web_studio/client_action/view_editor/editors/kanban_legacy/kanban_editor_sidebar_legacy/properties/kanban_cover_properties/kanban_cover_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { getFieldsInArch } from \"@web_studio/client_action/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nclass KanbanFieldProperties extends FieldProperties {\n    onChangeAttribute(value, name) {\n        if (name === \"bold\" && !value) {\n            return this.editNodeAttributes({ [name]: \"\" });\n        }\n        return super.onChangeAttribute(...arguments);\n    }\n}\n\nexport class KanbanEditorSidebarLegacy extends Component {\n    static template = \"web_studio.ViewEditor.KanbanEditorSidebarLegacy\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        ExistingFields,\n        Property,\n        Properties,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n        this.propertiesComponents = {\n            field: {\n                component: KanbanFieldProperties,\n                props: {\n                    availableOptions: [\"invisible\", \"string\", \"bold\"],\n                },\n            },\n            t: {\n                component: KanbanCoverProperties,\n            },\n        };\n    }\n\n    get archInfo() {\n        return this.viewEditorModel.controllerProps.archInfo;\n    }\n\n    get defaultGroupBy() {\n        return {\n            choices: fieldsToChoices(\n                this.viewEditorModel.fields,\n                this.viewEditorModel.GROUPABLE_TYPES,\n                (field) => field.store\n            ),\n            required: false,\n        };\n    }\n\n    get kanbanFieldsInArch() {\n        // fields can be present in the xmlDoc to be preloaded, but not in\n        // the actual template. Those must be present in the sidebar\n        const kanbanXmlDoc = this.viewEditorModel.xmlDoc.querySelector(\"[t-name=kanban-box]\");\n        return getFieldsInArch(kanbanXmlDoc);\n    }\n\n    get defaultOrder() {\n        if (this.archInfo.defaultOrder.length >= 1) {\n            return this.archInfo.defaultOrder[0];\n        } else {\n            return { name: \"\", asc: true };\n        }\n    }\n\n    get sortChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) => field.store\n        );\n    }\n\n    get orderChoices() {\n        return [\n            { value: \"asc\", label: _t(\"Ascending\") },\n            { value: \"desc\", label: _t(\"Descending\") },\n        ];\n    }\n\n    setSortBy(value) {\n        this.onSortingChanged(value, this.defaultOrder.asc ? \"asc\" : \"desc\");\n    }\n\n    setOrder(value) {\n        this.onSortingChanged(this.defaultOrder.name, value);\n    }\n\n    onSortingChanged(sortBy, order) {\n        if (sortBy) {\n            this.editAttribute(`${sortBy} ${order}`, \"default_order\");\n        } else {\n            this.editAttribute(\"\", \"default_order\");\n        }\n    }\n\n    editAttribute(value, name) {\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    editDefaultGroupBy(value) {\n        this.editAttribute(value || \"\", \"default_group_by\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FieldSelectorDialog } from \"@web_studio/client_action/view_editor/editors/components/field_selector_dialog\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\n\nexport class KanbanCoverProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.KanbanCoverProperties\";\n    static props = {\n        node: { type: Object },\n    };\n    static components = { Property, SidebarPropertiesToolbox };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        // We don't want to display t in the sidebar.\n        this.env.viewEditorModel.activeNode.humanName = _t(\"Dropdown\");\n    }\n\n    get coverNode() {\n        return this.env.viewEditorModel.xmlDoc.querySelector(\n            \"a[data-type='set_cover'],a[type='set_cover']\"\n        );\n    }\n\n    get coverValue() {\n        return !!this.coverNode;\n    }\n\n    setCover(value, name) {\n        const fields = [];\n\n        for (const field of Object.values(this.env.viewEditorModel.fields)) {\n            if (field.type === \"many2one\" && field.relation === \"ir.attachment\") {\n                fields.push(field);\n            }\n        }\n\n        this.dialog.add(FieldSelectorDialog, {\n            fields: fields,\n            showNew: true,\n            onConfirm: (field) => {\n                const operation = {\n                    type: \"kanban_set_cover\",\n                    field: field,\n                };\n                this.env.viewEditorModel.doOperation(operation);\n            },\n        });\n    }\n\n    onChangeCover(value, name) {\n        if (!value) {\n            const vem = this.env.viewEditorModel;\n            const fieldToRemove = Object.entries(vem.controllerProps.archInfo.fieldNodes).filter(\n                ([fName, fInfo]) => {\n                    return fInfo.widget === \"attachment_image\";\n                }\n            );\n            if (fieldToRemove.length !== 1) {\n                return;\n            }\n\n            const extraNode = this.coverNode;\n            const relevantAttr = [\"type\", \"data-type\"].filter((att) => {\n                return extraNode.hasAttribute(att) && extraNode.getAttribute(att) === \"set_cover\";\n            })[0];\n            const operation = {\n                target: {\n                    attrs: { name: fieldToRemove[0][1].name },\n                    tag: \"field\",\n                    extra_nodes: [\n                        {\n                            tag: extraNode.tagName,\n                            attrs: {\n                                [relevantAttr]: \"set_cover\",\n                            },\n                        },\n                    ],\n                },\n                type: \"remove\",\n            };\n            vem.doOperation(operation);\n        } else {\n            this.setCover(value, name);\n        }\n    }\n}\n", "import { listView } from \"@web/views/list/list_view\";\nimport { computeXpath } from \"../xml_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { omit } from \"@web/core/utils/objects\";\n\nimport { ListEditorRenderer, columnsStyling } from \"./list_editor_renderer\";\n\nimport { Component, xml } from \"@odoo/owl\";\nimport { ListEditorSidebar } from \"./list_editor_sidebar/list_editor_sidebar\";\nimport { getStudioNoFetchFields, useModelConfigFetchInvisible } from \"../utils\";\n\nfunction parseStudioGroups(node) {\n    if (node.hasAttribute(\"studio_groups\")) {\n        return node.getAttribute(\"studio_groups\");\n    }\n}\n\nclass EditorArchParser extends listView.ArchParser {\n    parse(arch, models, modelName) {\n        const parsed = super.parse(...arguments);\n        const noFetch = getStudioNoFetchFields(parsed.fieldNodes);\n        parsed.fieldNodes = omit(parsed.fieldNodes, ...noFetch.fieldNodes);\n        const noFetchFieldNames = noFetch.fieldNames;\n        parsed.columns = parsed.columns.filter(\n            (col) => col.type !== \"field\" || !noFetchFieldNames.includes(col.name)\n        );\n        return parsed;\n    }\n\n    parseFieldNode(node, models, modelName) {\n        const parsed = super.parseFieldNode(...arguments);\n        parsed.studioXpath = computeXpath(node, \"list, tree\");\n        parsed.studio_groups = parseStudioGroups(node);\n        return parsed;\n    }\n\n    parseWidgetNode(node, models, modelName) {\n        const parsed = super.parseWidgetNode(...arguments);\n        parsed.studioXpath = computeXpath(node, \"list, tree\");\n        parsed.studio_groups = parseStudioGroups(node);\n        return parsed;\n    }\n\n    processButton(node) {\n        const parsed = super.processButton(node);\n        parsed.studioXpath = computeXpath(node, \"list, tree\");\n        parsed.studio_groups = parseStudioGroups(node);\n        return parsed;\n    }\n}\n\n/**\n * X2Many fields can have their subview edited. There are some challenges currently with the RelationalModel\n * - We need to inject the parent record in the evalContext. That way, within the subview's arch\n *   a snippet like `<field name=\"...\" invisible=\"not parent.id\" />` works.\n * - We already know the resIds we have, since we are coming from a x2m. There is no need to search_read them, just to read them\n * - The RelationalModel doesn't really supports creatic staticLists as the root record\n *\n * StaticList supports the two first needs and not DynamicList, we assume that the amount of hacking\n * would be slightly bigger if our starting point is DynamicList. Hence, we choose\n * to extend StaticList instead of DynamicList, and make it the root record of the model.\n */\nfunction useParentedStaticList(model, parentRecord, resIds) {\n    const config = model.config;\n    config.resIds = resIds;\n    config.offset = 0;\n    config.limit = Math.max(7, resIds.length); // don't load everything\n\n    model._createRoot = (config, data) => {\n        const options = { parent: parentRecord };\n        const list = new model.constructor.StaticList(model, { ...config }, data, options);\n        list.selection = [];\n        return list;\n    };\n}\n\nclass ListEditorController extends listView.Controller {\n    static props = {\n        ...listView.Controller.props,\n        parentRecord: { type: Object, optional: true },\n    };\n    setup() {\n        super.setup();\n        useModelConfigFetchInvisible(this.model);\n        if (this.props.parentRecord) {\n            useParentedStaticList(this.model, this.props.parentRecord, this.props.resIds);\n        }\n    }\n}\n\nclass ControllerShadow extends Component {\n    static props = { ...ListEditorController.props };\n    static template = xml`<t t-component=\"Component\" t-props=\"componentProps\" />`;\n    get Component() {\n        return ListEditorController;\n    }\n\n    get componentProps() {\n        const props = { ...this.props };\n        props.groupBy = [];\n        return props;\n    }\n}\n\nconst listEditor = {\n    ...listView,\n    Controller: ControllerShadow,\n    ArchParser: EditorArchParser,\n    Renderer: ListEditorRenderer,\n    props() {\n        const props = listView.props(...arguments);\n        props.allowSelectors = false;\n        props.editable = false;\n        props.showButtons = false;\n        return props;\n    },\n    Sidebar: ListEditorSidebar,\n};\nregistry.category(\"studio_editors\").add(\"list\", listEditor);\n\n/**\n *  Drag/Drop styling\n */\n\nconst colNearestHookClass = \"o_web_studio_nearest_hook\";\nlistEditor.styleNearestHook = function styleNearestColumn(mainRef, nearestHook) {\n    const xpath = nearestHook.dataset.xpath;\n    const position = nearestHook.dataset.position;\n    columnsStyling(\n        mainRef.el,\n        `.o_web_studio_hook[data-xpath='${xpath}'][data-position='${position}']`,\n        [colNearestHookClass]\n    );\n};\n\nlistEditor.styleClickedElement = (mainRef, params) => {\n    columnsStyling(mainRef.el, `[data-studio-xpath='${params.xpath}']:not(.o_web_studio_hook)`, [\n        \"o-web-studio-editor--element-clicked\",\n    ]);\n};\n", "/** @odoo-module */\nimport { listView } from \"@web/views/list/list_view\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { reactive, useEffect, useState } from \"@odoo/owl\";\nimport { AddButtonAction } from \"../../interactive_editor/action_button/action_button\";\n\nconst colSelectedClass = \"o-web-studio-editor--element-clicked\";\nconst colHoverClass = \"o-web-studio--col-hovered\";\n\nfunction cleanStyling(mainEl, classNames) {\n    mainEl.querySelectorAll(`${classNames.map((c) => `.${c}`)}`).forEach((el) => {\n        el.classList.remove(...classNames);\n    });\n}\n\nexport function columnsStyling(mainEl, colSelector, classNames) {\n    mainEl.querySelectorAll(`td${colSelector}, th${colSelector}`).forEach((el) => {\n        el.classList.add(...classNames);\n    });\n}\n\nfunction getSelectableCol(target, colSelector) {\n    if (target.closest(\"button\")) {\n        return null;\n    }\n    const colEl = target.closest(`td${colSelector}, th${colSelector}`);\n    return colEl;\n}\n\nexport class ListEditorRenderer extends listView.Renderer {\n    static template = \"web_studio.ListEditorRenderer\";\n    static recordRowTemplate = \"web_studio.ListEditorRenderer.RecordRow\";\n    static components = {\n        ...listView.Renderer.components,\n        AddButtonAction,\n    };\n\n    setup() {\n        const viewEditorModel = useState(this.env.viewEditorModel);\n        this.viewEditorModel = reactive(viewEditorModel, () => {\n            // Little trick to update our columns when showInvisible changes on the viewEditorModel\n            // getActiveColumns reads that value\n            this.columns = this.getActiveColumns(this.props.list);\n            this.render();\n        });\n        super.setup();\n        this.onTableHover = useThrottleForAnimation(this.onTableHover);\n\n        useEffect(\n            (rootEl) => {\n                rootEl.classList.add(\"o_web_studio_list_view_editor\");\n            },\n            () => [this.rootRef.el]\n        );\n    }\n\n    get canResequenceRows() {\n        return false;\n    }\n\n    getColumnClass(col) {\n        let cls = super.getColumnClass(col);\n        if (col.studioColumnInvisible) {\n            cls += \" o_web_studio_show_invisible\";\n        }\n        return cls;\n    }\n\n    getCellClass(col, record) {\n        let cls = super.getCellClass(col, record);\n        if (col.studioColumnInvisible || super.evalInvisible(col.invisible, record)) {\n            cls += \" o_web_studio_show_invisible\";\n        }\n        return cls;\n    }\n\n    getColumnHookData(col, position) {\n        let xpath;\n        if (!col) {\n            return { xpath: \"/list\", position: \"inside\" };\n        }\n        if (col.type === \"button_group\") {\n            if (position === \"before\") {\n                xpath = col.buttons[0].studioXpath;\n            } else {\n                xpath = col.buttons[col.buttons.length - 1].studioXpath;\n            }\n        } else {\n            xpath = col.studioXpath;\n        }\n        return {\n            xpath,\n            position,\n        };\n    }\n\n    addColsHooks(_cols) {\n        const attrs = { width: \"4px\" };\n        const options = {};\n        const cols = [];\n        let hookId = 0;\n        const firstCol = _cols.find((c) => c.optional !== \"hide\");\n        const { xpath, position } = this.getColumnHookData(firstCol, \"before\");\n        cols.push({\n            type: \"studio_hook\",\n            position,\n            xpath,\n            id: `studio_hook_${hookId++}_${(firstCol && firstCol.id) || 0}`,\n            attrs,\n            options,\n        });\n        for (const col of _cols) {\n            if (col.optional === \"hide\") {\n                continue;\n            }\n            cols.push(col);\n            const { xpath, position } = this.getColumnHookData(col, \"after\");\n            cols.push({\n                type: \"studio_hook\",\n                position,\n                xpath,\n                id: `studio_hook_${hookId++}_${col.id}`,\n                attrs,\n                options,\n            });\n        }\n        return cols;\n    }\n\n    get allColumns() {\n        let cols = this._allColumns;\n        if (this.viewEditorModel.showInvisible) {\n            cols = cols.map((c) => {\n                return {\n                    ...c,\n                    optional: false,\n                    studioColumnInvisible:\n                        c.optional === \"hide\" || super.evalColumnInvisible(c.column_invisible),\n                };\n            });\n        } else {\n            cols = cols.filter((c) => !this.evalColumnInvisible(c.column_invisible));\n        }\n        return this.addColsHooks(cols);\n    }\n\n    set allColumns(cols) {\n        this._allColumns = cols;\n    }\n\n    evalInvisible(modifier, record) {\n        if (this.viewEditorModel.showInvisible) {\n            return false;\n        }\n        return super.evalInvisible(modifier, record);\n    }\n    evalColumnInvisible(columnInvisible) {\n        if (this.viewEditorModel.showInvisible) {\n            return false;\n        }\n        return super.evalColumnInvisible(columnInvisible);\n    }\n\n    onTableHover(ev) {\n        const table = this.tableRef.el;\n        cleanStyling(table, [colHoverClass]);\n        if (ev.type !== \"mouseover\") {\n            return;\n        }\n        const colEl = getSelectableCol(ev.target, \"[data-studio-xpath]\");\n        if (!colEl) {\n            return;\n        }\n        const xpath = colEl.dataset.studioXpath;\n        columnsStyling(table, `[data-studio-xpath='${xpath}']:not(.o_web_studio_hook)`, [\n            colHoverClass,\n        ]);\n    }\n\n    onTableClicked(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        const table = ev.currentTarget;\n        cleanStyling(table, [colSelectedClass]);\n        const colEl = getSelectableCol(ev.target, \"[data-studio-xpath]\");\n        if (!colEl) {\n            return;\n        }\n        this.env.config.onNodeClicked(colEl.dataset.studioXpath);\n    }\n\n    makeTooltipButton(button) {\n        return JSON.stringify({\n            button: {\n                string: button.string,\n                type: button.clickParams?.type,\n                name: button.clickParams?.name,\n            },\n            debug: true,\n        });\n    }\n}\n", "/** @odoo-module */\nimport { Component, useState } from \"@odoo/owl\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport {\n    ExistingFields,\n    NewFields,\n} from \"@web_studio/client_action/view_editor/editors/components/view_fields\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { Properties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/properties\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { FieldProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/field_properties/field_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\n\nclass ListFieldNodeProperties extends FieldProperties {\n    onChangeAttribute(value, name) {\n        if (name !== \"aggregate\") {\n            return super.onChangeAttribute(...arguments);\n        }\n        const activeNode = this.env.viewEditorModel.activeNode;\n        const newAttrs = {\n            avg: \"\",\n            sum: \"\",\n        };\n        if (value && value !== \"none\") {\n            const humanName = value === \"sum\" ? _t(\"Sum of %s\") : _t(\"Average of %s\");\n            const fieldString = activeNode.attrs.string || activeNode.field.label;\n            newAttrs[value] = sprintf(humanName, fieldString);\n        }\n        return this.editNodeAttributes(newAttrs);\n    }\n}\n\nexport class ListEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.ListEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        NewFields,\n        ExistingFields,\n        Property,\n        Properties,\n        SidebarViewToolbox,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n        this.propertiesComponents = {\n            field: {\n                component: ListFieldNodeProperties,\n                props: {\n                    availableOptions: [\n                        \"invisible\",\n                        \"required\",\n                        \"readonly\",\n                        \"string\",\n                        \"help\",\n                        \"optional\",\n                    ],\n                },\n            },\n        };\n    }\n\n    get archInfo() {\n        return this.viewEditorModel.controllerProps.archInfo;\n    }\n\n    get defaultOrder() {\n        if (this.archInfo.defaultOrder.length >= 1) {\n            return this.archInfo.defaultOrder[0];\n        } else {\n            return { name: \"\", asc: true };\n        }\n    }\n\n    get editableChoices() {\n        return [\n            { value: \"\", label: _t(\"Open form view\") },\n            { value: \"top\", label: _t(\"Add record on top\") },\n            { value: \"bottom\", label: _t(\"Add record at the bottom\") },\n        ];\n    }\n\n    get sortChoices() {\n        // only have stored fields that are present in arch\n        const storeFieldsInArch = Object.fromEntries(\n            Object.values(this.archInfo.fieldNodes).map((field) => [field.name, this.viewEditorModel.fields[field.name]])\n        );\n        return fieldsToChoices(\n            storeFieldsInArch,\n            null,\n            (field) => ![\"one2many\", \"many2many\", \"binary\"].includes(field.type) && field.store\n        );\n    }\n\n    get orderChoices() {\n        return [\n            { value: \"asc\", label: _t(\"Ascending\") },\n            { value: \"desc\", label: _t(\"Descending\") },\n        ];\n    }\n\n    get defaultGroupbyChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) => field.store\n        );\n    }\n\n    setSortBy(value) {\n        this.onSortingChanged(value, this.defaultOrder.asc ? \"asc\" : \"desc\");\n    }\n\n    setOrder(value) {\n        this.onSortingChanged(this.defaultOrder.name, value);\n    }\n\n    onSortingChanged(sortBy, order) {\n        if (sortBy) {\n            this.onAttributeChanged(`${sortBy} ${order}`, \"default_order\");\n        } else {\n            this.onAttributeChanged(\"\", \"default_order\");\n        }\n    }\n\n    onAttributeChanged(value, name) {\n        return this.editArchAttributes({ [name]: value });\n    }\n}\n", "/** @odoo-module */\n\nimport { mapView } from \"@web_map/map_view/map_view\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { Record } from \"@web/model/record\";\nimport { Many2ManyTagsField } from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\n\nexport class MapEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.MapEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        Property,\n        SidebarViewToolbox,\n        Record,\n        Many2ManyTagsField,\n        MultiRecordSelector,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n    }\n\n    get modelParams() {\n        return this.viewEditorModel.controllerProps.modelParams;\n    }\n\n    get multiRecordSelectorProps() {\n        return {\n            resModel: \"ir.model.fields\",\n            update: this.changeAdditionalFields.bind(this),\n            resIds: this.currentAdditionalFieldsIds,\n            domain: [\n                [\"model\", \"=\", this.viewEditorModel.resModel],\n                [\"ttype\", \"not in\", [\"many2many\", \"one2many\", \"binary\"]],\n            ],\n        };\n    }\n\n    get currentAdditionalFieldsIds() {\n        return (\n            JSON.parse(\n                this.viewEditorModel.xmlDoc.firstElementChild.getAttribute(\"studio_map_field_ids\")\n            ) || []\n        );\n    }\n\n    onViewAttributeChanged(value, name) {\n        value = value ? value : \"\";\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    get contactFieldChoices() {\n        return Object.values(this.viewEditorModel.fields)\n            .filter((field) => field.type === \"many2one\" && field.relation === \"res.partner\")\n            .map((field) => ({ label: `${field.string} (${field.name})`, value: field.name }));\n    }\n\n    get defaultOrderChoices() {\n        return Object.values(this.viewEditorModel.fields)\n            .filter(\n                (field) => field.store && ![\"one2many\", \"many2many\", \"binary\"].includes(field.type)\n            )\n            .map((field) => ({ label: `${field.string} (${field.name})`, value: field.name }));\n    }\n\n    /**\n     * @param {Array<Number>} resIds\n     */\n    changeAdditionalFields(resIds) {\n        const currentFullIds = this.currentAdditionalFieldsIds;\n        const newIds = resIds.filter((id) => !currentFullIds.includes(id));\n        let toRemoveIds;\n\n        const operationType = newIds.length ? \"add\" : \"remove\";\n\n        if (operationType === \"remove\") {\n            toRemoveIds = currentFullIds.filter((id) => !resIds.includes(id));\n        }\n\n        this.viewEditorModel.doOperation({\n            type: \"map_popup_fields\",\n            target: {\n                operation_type: operationType,\n                field_ids: operationType === \"add\" ? newIds : toRemoveIds,\n            },\n        });\n    }\n}\n\nregistry.category(\"studio_editors\").add(\"map\", {\n    ...mapView,\n    Sidebar: MapEditorSidebar,\n});\n", "/** @odoo-module */\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\n\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport * as operationUtils from \"@web_studio/client_action/view_editor/operations_utils\";\nimport { fieldsToChoices } from \"@web_studio/client_action/view_editor/editors/utils\";\n\nimport { Record } from \"@web/model/record\";\nimport { Many2ManyTagsField } from \"@web/views/fields/many2many_tags/many2many_tags_field\";\n\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { computeReportMeasures } from \"@web/views/utils\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\n\nfunction getFieldNameFromGroupby(str) {\n    return str.split(\":\")[0];\n}\n\nexport class PivotEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.PivotEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        Property,\n        SidebarViewToolbox,\n        Record,\n        Many2ManyTagsField,\n        MultiRecordSelector,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        this.editArchAttributes = useEditNodeAttributes({ isRoot: true });\n    }\n\n    get possibleMeasures() {\n        const { fieldAttrs, activeMeasures } = this.archInfo;\n        return computeReportMeasures(this.viewEditorModel.fields, fieldAttrs, activeMeasures);\n    }\n\n    get multiRecordSelectorProps() {\n        return {\n            resModel: \"ir.model.fields\",\n            update: this.changeMeasureFields.bind(this),\n            resIds: this.currentMeasureFields,\n            domain: [\n                [\"model\", \"=\", this.viewEditorModel.resModel],\n                [\"name\", \"in\", Object.keys(this.possibleMeasures)],\n            ],\n        };\n    }\n\n    get currentMeasureFields() {\n        return (\n            JSON.parse(\n                this.viewEditorModel.xmlDoc.firstElementChild.getAttribute(\n                    \"studio_pivot_measure_field_ids\"\n                )\n            ) || []\n        );\n    }\n\n    get archInfo() {\n        return this.viewEditorModel.controllerProps.modelParams.metaData;\n    }\n\n    get rowGroupBys() {\n        return this.archInfo.rowGroupBys.map((fName) => getFieldNameFromGroupby(fName));\n    }\n\n    get colGroupBys() {\n        return this.archInfo.colGroupBys.map((fName) => getFieldNameFromGroupby(fName));\n    }\n\n    /**\n     * @param {Array<Number>} resIds\n     */\n    changeMeasureFields(resIds) {\n        const currentFullIds = this.currentMeasureFields;\n        const newIds = resIds.filter((id) => !currentFullIds.includes(id));\n        let toRemoveIds;\n\n        const operationType = newIds.length ? \"add\" : \"remove\";\n\n        if (operationType === \"remove\") {\n            toRemoveIds = currentFullIds.filter((id) => !resIds.includes(id));\n        }\n\n        this.viewEditorModel.doOperation({\n            type: \"pivot_measures_fields\",\n            target: {\n                operation_type: operationType,\n                field_ids: operationType === \"add\" ? newIds : toRemoveIds,\n            },\n        });\n    }\n\n    onGroupByChanged(type, newValue, oldValue) {\n        const operation = operationUtils.viewGroupByOperation(\"pivot\", type, newValue, oldValue);\n        this.viewEditorModel.doOperation(operation);\n    }\n\n    onViewAttributeChanged(value, name) {\n        value = value ? value : \"\";\n        return this.editArchAttributes({ [name]: value });\n    }\n\n    get columnGroupbyChoices() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) =>\n                field.store &&\n                ![this.archInfo.rowGroupBys[0], this.archInfo.rowGroupBys[1]].includes(field.name)\n        );\n    }\n\n    get rowGroupbyChoices_first() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) =>\n                field.store &&\n                ![this.archInfo.colGroupBys[0], this.archInfo.rowGroupBys[1]].includes(field.name)\n        );\n    }\n\n    get rowGroupbyChoices_second() {\n        return fieldsToChoices(\n            this.viewEditorModel.fields,\n            this.viewEditorModel.GROUPABLE_TYPES,\n            (field) =>\n                field.store &&\n                ![this.archInfo.colGroupBys[0], this.archInfo.rowGroupBys[0]].includes(field.name)\n        );\n    }\n}\n\nregistry.category(\"studio_editors\").add(\"pivot\", {\n    ...pivotView,\n    Sidebar: PivotEditorSidebar,\n});\n", "import { Component, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeXpath } from \"@web_studio/client_action/view_editor/editors/xml_utils\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { SidebarDraggableItem } from \"@web_studio/client_action/components/sidebar_draggable_item/sidebar_draggable_item\";\nimport { StudioHook } from \"@web_studio/client_action/view_editor/editors/components/studio_hook_component\";\nimport { InteractiveEditorSidebar } from \"@web_studio/client_action/view_editor/interactive_editor/interactive_editor_sidebar\";\nimport { ExistingFields } from \"@web_studio/client_action/view_editor/editors/components/view_fields\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SidebarViewToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/sidebar_view_toolbox/sidebar_view_toolbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport {\n    FieldConfigurationDialog,\n    FilterConfiguration,\n} from \"@web_studio/client_action/view_editor/interactive_editor/field_configuration/field_configuration\";\nimport { randomName } from \"@web_studio/client_action/view_editor/editors/utils\";\n\nfunction getGroupByFieldNameFromString(str) {\n    const matches = str.match(/(,\\s*)?([\"'])group_by\\2\\1(\\s*:\\s*)?\\2(?<fieldName>.*)\\2/);\n    if (!matches) {\n        return null;\n    }\n    if (!matches.groups) {\n        return null;\n    }\n    return matches.groups.fieldName;\n}\n\nfunction isFilterGroupBy(node) {\n    if (!node.hasAttribute(\"context\")) {\n        return false;\n    }\n    if (/(['\"])group_by\\1\\s*:/.test(node.getAttribute(\"context\"))) {\n        return true;\n    }\n    return false;\n}\n\n/** CONTROLLER STUFF */\nclass SearchEditorArchParser {\n    parse(xmlDoc) {\n        this.fields = [];\n        this.filters = [];\n        this.groupBys = [];\n        this.currentCategory = null;\n        this.currentItems = { items: [] };\n\n        visitXML(xmlDoc, this.visitNode.bind(this));\n        this.changeCategory(null, true); // Flush\n\n        return {\n            fields: this.fields,\n            filters: this.filters,\n            groupBys: this.groupBys,\n            xmlDoc,\n        };\n    }\n\n    visitNode(node) {\n        if (node.nodeType !== 1) {\n            return;\n        }\n\n        const nodeName = node.nodeName;\n        const studioXpath = computeXpath(node, \"search\");\n        if (nodeName === \"field\") {\n            this.changeCategory(\"field\", true);\n            const item = this.parseNode(node);\n            item.studioXpath = studioXpath;\n            this.fields.push(item);\n            return false;\n        }\n        if (nodeName === \"filter\") {\n            const category = isFilterGroupBy(node) ? \"groupBy\" : \"filter\";\n            this.changeCategory(category);\n            const item = this.parseNode(node);\n            item.studioXpath = studioXpath;\n            this.pushItem(item);\n            return false;\n        }\n        if (nodeName === \"separator\") {\n            this.changeCategory(\"filter\", true);\n            this.currentItems.separator = studioXpath;\n            return false;\n        }\n        if (nodeName === \"group\") {\n            this.changeCategory(null, true);\n            Array.from(node.children).forEach(this.visitNode.bind(this));\n            this.changeCategory(null, true);\n            return false;\n        }\n    }\n\n    parseNode(node) {\n        const nodeName = node.nodeName;\n        const invisible = node.getAttribute(\"invisible\");\n        if (nodeName === \"field\") {\n            return {\n                type: \"field\",\n                name: node.getAttribute(\"name\"),\n                label: node.getAttribute(\"string\"),\n                invisible,\n            };\n        }\n        if (nodeName === \"separator\") {\n            return { type: \"separator\" };\n        }\n        if (nodeName === \"filter\") {\n            const item = {\n                type: \"filter\",\n                name: node.getAttribute(\"name\"),\n                label: node.getAttribute(\"string\") || node.getAttribute(\"help\"),\n                domain: node.getAttribute(\"domain\"),\n                invisible,\n            };\n            if (node.hasAttribute(\"context\")) {\n                const groupBy = getGroupByFieldNameFromString(node.getAttribute(\"context\"));\n                if (groupBy) {\n                    item.groupBy = groupBy;\n                    item.type = \"groupBy\";\n                }\n            }\n            return item;\n        }\n    }\n\n    pushItem(item) {\n        this.currentItems.items.push(item);\n    }\n\n    changeCategory(category, force) {\n        if (this.currentCategory !== category || force) {\n            let itemsToPushIn;\n            if (this.currentCategory === \"filter\") {\n                itemsToPushIn = this.filters;\n            } else if (this.currentCategory === \"groupBy\") {\n                itemsToPushIn = this.groupBys;\n            }\n            if (itemsToPushIn) {\n                itemsToPushIn.push(this.currentItems);\n                this.currentItems = { items: [] };\n            }\n        }\n        this.currentCategory = category || this.currentCategory;\n    }\n}\n\nclass SearchEditorController extends Component {\n    static props = { ...standardViewProps, archInfo: { type: Object } };\n    static template = \"web_studio.SearchEditorController\";\n    static components = { StudioHook };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n    }\n\n    get filtersGroups() {\n        return this.props.archInfo.filters;\n    }\n\n    hasItems(group) {\n        return group.some((g) => this.getItems(g.items));\n    }\n\n    get autoCompleteFields() {\n        return this.props.archInfo.fields;\n    }\n\n    get groupByGroups() {\n        return this.props.archInfo.groupBys;\n    }\n\n    getItems(items) {\n        if (!this.viewEditorModel.showInvisible) {\n            return items.filter((i) => i.invisible !== \"True\" && i.invisible !== \"1\");\n        }\n        return items;\n    }\n\n    getFirstHookProps(type) {\n        const xpath = \"/search\";\n        const position = \"inside\";\n\n        const group = type === \"filter\" ? this.filtersGroups : this.groupByGroups;\n        if (this.hasItems(group)) {\n            return false;\n        }\n        const props = {\n            xpath,\n            position,\n            type,\n        };\n        if (type === \"groupBy\") {\n            props.infos = JSON.stringify({\n                create_group: true,\n            });\n        }\n        return props;\n    }\n\n    getItemLabel(type, item) {\n        if (type === \"filter\") {\n            return item.label;\n        }\n        if (type === \"groupBy\") {\n            let label = item.label || item.name;\n            if (this.env.debug) {\n                label = `${label} (${item.groupBy})`;\n            }\n            return label;\n        }\n        if (type === \"field\") {\n            let label = item.label || this.props.fields[item.name].string;\n            if (this.env.debug) {\n                label = `${label} (${item.name})`;\n            }\n            return label;\n        }\n    }\n\n    onItemClicked(ev, xpath) {\n        this.env.config.onNodeClicked(xpath);\n    }\n}\n\n/** SIDEBAR STUFF */\n\nclass SearchComponents extends Component {\n    static components = { SidebarDraggableItem };\n    static props = {};\n    static template = \"web_studio.SearchEditor.Sidebar.Components\";\n\n    get structures() {\n        return {\n            filter: {\n                name: _t(\"Filter\"),\n                class: \"o_web_studio_filter\",\n            },\n            separator: {\n                name: _t(\"Separator\"),\n                class: \"o_web_studio_filter_separator\",\n            },\n        };\n    }\n}\n\nclass SimpleElementEditor extends Component {\n    static props = { node: { type: Object } };\n    static components = { Property, SidebarPropertiesToolbox };\n    static template = \"web_studio.SearchEditor.SimpleElementEditor\";\n\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n\n    get viewEditorModel() {\n        return this.env.viewEditorModel;\n    }\n\n    get node() {\n        return this.props.node;\n    }\n\n    get label() {\n        if (this.node.type === \"field\" && !this.node.label) {\n            return this.env.viewEditorModel.fields[this.node.name].string;\n        }\n        return this.node.label;\n    }\n\n    get domain() {\n        if (this.node.type === \"filter\") {\n            return this.node.domain;\n        }\n        return null;\n    }\n\n    onChangeDomain(value) {\n        const operation = {\n            new_attrs: { domain: value },\n            type: \"attributes\",\n            position: \"attributes\",\n            target: this.viewEditorModel.getFullTarget(this.viewEditorModel.activeNodeXpath),\n        };\n        this.viewEditorModel.doOperation(operation);\n    }\n\n    onChangeLabel(value) {\n        const operation = {\n            new_attrs: { string: value },\n            type: \"attributes\",\n            position: \"attributes\",\n            target: this.viewEditorModel.getFullTarget(this.viewEditorModel.activeNodeXpath),\n        };\n        this.viewEditorModel.doOperation(operation);\n    }\n\n    onPropertyRemoved() {\n        const activeNodeXpath = this.viewEditorModel.activeNodeXpath;\n        this.viewEditorModel.activeNodeXpath = null;\n        const operation = {\n            type: \"remove\",\n            target: this.viewEditorModel.getFullTarget(activeNodeXpath),\n        };\n        this.viewEditorModel.doOperation(operation);\n    }\n}\n\nclass SearchEditorSidebar extends Component {\n    static template = \"web_studio.ViewEditor.SearchEditorSidebar\";\n    static props = {\n        openViewInForm: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n    };\n    static components = {\n        InteractiveEditorSidebar,\n        ExistingFields,\n        SearchComponents,\n        Property,\n        SidebarViewToolbox,\n        SimpleElementEditor,\n    };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n        const searchArchParser = new SearchEditorArchParser();\n        this._getCurrentNode = memoize(() =>\n            searchArchParser.parseNode(this.viewEditorModel.activeNode.arch)\n        );\n    }\n\n    get currentNode() {\n        const { activeNodeXpath, arch } = this.viewEditorModel;\n        return this._getCurrentNode(`${activeNodeXpath}_${arch}`);\n    }\n}\n\n/** SIDEBAR STUFF */\nconst searchEditor = {\n    ArchParser: SearchEditorArchParser,\n    Controller: SearchEditorController,\n    props(genericProps, editor, config) {\n        const archInfo = new editor.ArchParser().parse(genericProps.arch);\n        return { ...genericProps, archInfo };\n    },\n    Sidebar: SearchEditorSidebar,\n};\n\nregistry.category(\"studio_editors\").add(\"search\", searchEditor);\n\nasync function addSearchViewStructure(structure, { droppedData, targetInfo, addDialog }) {\n    switch (structure) {\n        case \"group\":\n        case \"separator\": {\n            return {\n                node: {\n                    tag: structure,\n                    attrs: {\n                        name: randomName(`studio_${structure}`),\n                    },\n                },\n            };\n        }\n\n        case \"field\": {\n            if (![\"groupBy\", \"filter\"].includes(targetInfo.type)) {\n                return;\n            } else {\n                const fieldName = JSON.parse(droppedData).fieldName;\n                const fieldGet = this.env.viewEditorModel.fields[fieldName];\n                const willBeGroupBy = targetInfo.type === \"groupBy\";\n\n                const node = {\n                    tag: \"filter\",\n                    attrs: {\n                        name: randomName(`studio_${willBeGroupBy ? \"group\" : \"filter\"}_by`),\n                        string: fieldGet.string,\n                    },\n                };\n\n                if (willBeGroupBy) {\n                    node.attrs.context = `{'group_by': '${fieldName}'}`;\n                    if (targetInfo.infos && JSON.parse(targetInfo.infos).create_group) {\n                        node.attrs.create_group = true;\n                    }\n                } else {\n                    node.attrs.date = fieldName;\n                }\n                return {\n                    node,\n                };\n            }\n        }\n        case \"filter\": {\n            const filterData = await new Promise((resolve) => {\n                addDialog(FieldConfigurationDialog, {\n                    title: _t(\"New Filter\"),\n                    size: \"md\",\n                    confirm: (data) => resolve(data),\n                    cancel: () => resolve(false),\n                    Component: FilterConfiguration,\n                    componentProps: { resModel: this.env.viewEditorModel.resModel },\n                });\n            });\n            if (!filterData) {\n                return;\n            }\n            const node = {\n                tag: \"filter\",\n                attrs: {\n                    domain: filterData.domain,\n                    name: randomName(\"studio_filter\"),\n                    string: filterData.filterLabel,\n                },\n            };\n            return {\n                node,\n            };\n        }\n    }\n}\nsearchEditor.addViewStructure = addSearchViewStructure;\n\n/** Drag/Drop */\n\nconst FILTER_TYPES = [\"date\", \"datetime\"];\nconst GROUPABLE_TYPES = [\n    \"many2one\",\n    \"many2many\",\n    \"char\",\n    \"boolean\",\n    \"selection\",\n    \"date\",\n    \"datetime\",\n];\n\nfunction fieldCanBeFilter(field) {\n    return FILTER_TYPES.includes(field.type) && field.store;\n}\n\nfunction fieldCanBeGroupable(field) {\n    return GROUPABLE_TYPES.includes(field.type) && field.store;\n}\n\nconst disabledDropClass = \"o-web-studio-search--drop-disable\";\n\nsearchEditor.isValidHook = function isValidSearchHook({ hook, element, viewEditorModel }) {\n    if (hook.closest(`.${disabledDropClass}`)) {\n        return false;\n    }\n    return true;\n};\n\nsearchEditor.prepareForDrag = function ({ element, viewEditorModel, ref }) {\n    const draggingStructure = element.dataset.structure;\n\n    switch (draggingStructure) {\n        case \"field\": {\n            const fieldName = JSON.parse(element.dataset.drop).fieldName;\n            const field = viewEditorModel.fields[fieldName];\n            if (!fieldCanBeFilter(field)) {\n                ref.el\n                    .querySelector(`.o-web-studio-search--filters`)\n                    .classList.add(disabledDropClass);\n            }\n            if (!fieldCanBeGroupable(field)) {\n                ref.el\n                    .querySelector(`.o-web-studio-search--groupbys`)\n                    .classList.add(disabledDropClass);\n            }\n\n            break;\n        }\n        case \"separator\":\n        case \"filter\": {\n            const els = ref.el.querySelectorAll(\n                \".o-web-studio-search--fields,.o-web-studio-search--groupbys\"\n            );\n            els.forEach((el) => el.classList.add(\"o-web-studio-search--drop-disable\"));\n            break;\n        }\n    }\n\n    return () => {\n        ref.el\n            .querySelectorAll(\".o-web-studio-search--drop-disable\")\n            .forEach((el) => el.classList.remove(\"o-web-studio-search--drop-disable\"));\n    };\n};\n", "/** @odoo-module */\n\n/**\n * A list of field widget keys of the wowl's field registry (`registry.category(\"fields\")`)\n * that are safe for the user to swith to when editing a field's properties in the view editor's sidebar.\n *\n * Other widgets either don't make sense for that use because they are too specific, or they need\n * specific implementation details provided by some view to be usable.\n */\nexport const SIDEBAR_SAFE_FIELDS = [\n    \"badge\",\n    \"selection_badge\",\n    \"handle\",\n    \"percentpie\",\n    \"radio\",\n    \"selection\",\n    \"image_url\",\n    \"ace\",\n    \"priority\",\n    \"date\",\n    \"datetime\",\n    \"remaining_days\",\n    \"email\",\n    \"phone\",\n    \"url\",\n    \"binary\",\n    \"image\",\n    \"pdf_viewer\",\n    \"boolean\",\n    \"state_selection\",\n    \"boolean_toggle\",\n    \"statusbar\",\n    \"float\",\n    \"float_time\",\n    \"integer\",\n    \"monetary\",\n    \"percentage\",\n    \"progressbar\",\n    \"text\",\n    \"boolean_favorite\",\n    \"boolean_icon\",\n    \"char\",\n    \"statinfo\",\n    \"html\",\n    \"text_emojis\",\n    \"CopyClipboardChar\",\n    \"CopyClipboardURL\",\n    \"char_emojis\",\n    \"many2many_tags\",\n    \"many2one\",\n    \"many2many\",\n    \"one2many\",\n    \"sms_widget\",\n    \"reference\",\n    \"daterange\",\n];\n", "/** @odoo-module */\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { registry } from \"@web/core/registry\";\nimport { SIDEBAR_SAFE_FIELDS } from \"@web_studio/client_action/view_editor/editors/sidebar_safe_fields\";\nimport { useComponent, useEffect, useRef } from \"@odoo/owl\";\n\nexport const hookPositionTolerance = 50;\n\nexport function cleanHooks(el) {\n    for (const hookEl of el.querySelectorAll(\".o_web_studio_nearest_hook\")) {\n        hookEl.classList.remove(\"o_web_studio_nearest_hook\");\n    }\n}\n\nexport function getActiveHook(el) {\n    return el.querySelector(\".o_web_studio_nearest_hook\");\n}\n\n// A naive function that determines if the toXpath on which we dropped\n// our object is actually the same as the fromXpath of the element we dropped.\n// Naive because it won't evaluate xpath, just guess whether they are equivalent\n// under precise conditions.\nexport function isToXpathEquivalentFromXpath(position, toXpath, fromXpath) {\n    if (toXpath === fromXpath) {\n        return true;\n    }\n    const toParts = toXpath.split(\"/\");\n    const fromParts = fromXpath.split(\"/\");\n\n    // Are the paths at least in the same parent node ?\n    if (toParts.slice(0, -1).join(\"/\") !== fromParts.slice(0, -1).join(\"/\")) {\n        return false;\n    }\n\n    const nodeIdxRegExp = /(\\w+)(\\[(\\d+)\\])?/;\n    const toMatch = toParts[toParts.length - 1].match(nodeIdxRegExp);\n    const fromMatch = fromParts[fromParts.length - 1].match(nodeIdxRegExp);\n\n    // Are the paths comparable in terms of their node tag ?\n    if (fromMatch[1] !== toMatch[1]) {\n        return false;\n    }\n\n    // Is the position actually referring to the same place ?\n    if (position === \"after\" && parseInt(toMatch[3] || 1) + 1 === parseInt(fromMatch[3] || 1)) {\n        return true;\n    }\n    return false;\n}\n\nexport function getHooks(el) {\n    return [...el.querySelectorAll(\".o_web_studio_hook\")];\n}\n\nexport function randomName(baseName) {\n    const random =\n        Math.floor(Math.random() * 10000).toString(32) + \"_\" + Number(new Date()).toString(32);\n    return `${baseName}_${random}`;\n}\n\n// A standardized method to determine if a component is visible\nexport function studioIsVisible(props) {\n    return props.studioIsVisible !== undefined ? props.studioIsVisible : true;\n}\n\nexport function cleanClickedElements(mainEl) {\n    for (const el of mainEl.querySelectorAll(\".o-web-studio-editor--element-clicked\")) {\n        el.classList.remove(\"o-web-studio-editor--element-clicked\");\n    }\n}\n\nexport function useStudioRef(refName = \"studioRef\", onClick) {\n    // create two hooks and call them here?\n    const comp = useComponent();\n    const ref = useRef(refName);\n    useEffect(\n        (el) => {\n            if (el) {\n                el.setAttribute(\"data-studio-xpath\", comp.props.studioXpath);\n            }\n        },\n        () => [ref.el]\n    );\n\n    if (onClick) {\n        const handler = onClick.bind(comp);\n        useEffect(\n            (el) => {\n                if (el) {\n                    el.addEventListener(\"click\", handler, { capture: true });\n                    return () => {\n                        el.removeEventListener(\"click\", handler);\n                    };\n                }\n            },\n            () => [ref.el]\n        );\n    }\n}\n\nexport function makeModelErrorResilient(ModelClass) {\n    function logError(debug) {\n        if (!debug) {\n            return;\n        }\n        console.warn(\n            \"The onchange triggered an error. It may indicate either a faulty call to onchange, or a faulty model python side\"\n        );\n    }\n    return class ResilientModel extends ModelClass {\n        setup() {\n            super.setup(...arguments);\n            const orm = this.orm;\n            const debug = this.env.debug;\n            this.orm = Object.assign(Object.create(orm), {\n                async call(model, method) {\n                    if (method === \"onchange\") {\n                        try {\n                            return await orm.call.call(orm, ...arguments);\n                        } catch {\n                            logError(debug);\n                        }\n                        return { value: {} };\n                    }\n                    return orm.call.call(orm, ...arguments);\n                },\n            });\n        }\n    };\n}\n\nexport function getWowlFieldWidgets(\n    fieldType,\n    currentKey = \"\",\n    blacklistedKeys = [],\n    debug = false\n) {\n    const wowlFieldRegistry = registry.category(\"fields\");\n    const widgets = [];\n    for (const [widgetKey, Component] of wowlFieldRegistry.getEntries()) {\n        if (widgetKey !== currentKey) {\n            // always show the current widget\n            // Widget dosn't explicitly supports the field's type\n            if (!Component.supportedTypes || !Component.supportedTypes.includes(fieldType)) {\n                continue;\n            }\n            // Widget is view-specific or is blacklisted\n            if (widgetKey.includes(\".\") || blacklistedKeys.includes(widgetKey)) {\n                continue;\n            }\n            // Widget is not whitelisted\n            if (!debug && !SIDEBAR_SAFE_FIELDS.includes(widgetKey)) {\n                continue;\n            }\n        }\n        widgets.push([widgetKey, Component.displayName]);\n    }\n    return sortBy(widgets, (el) => el[1] || el[0]);\n}\n\nexport function xpathToLegacyXpathInfo(xpath) {\n    // eg: /form[1]/field[3]\n    // RegExp notice: group 1 : form ; group 2: [1], group 3: 1\n    const xpathInfo = [];\n    const matches = xpath.matchAll(/\\/?(\\w+|\\*)(\\[(\\d+)\\])?/g);\n    for (const m of matches) {\n        const info = {\n            tag: m[1],\n            indice: parseInt(m[3] || 1),\n        };\n        xpathInfo.push(info);\n    }\n    return xpathInfo;\n}\n\nexport function fieldsToChoices(fields, availableTypes, filterCallback) {\n    let values = Object.values(fields);\n    if (filterCallback) {\n        values = values.filter(filterCallback);\n    }\n    if (availableTypes) {\n        values = values.filter((f) => availableTypes.includes(f.type));\n    }\n    return values.map((field) => ({\n        label: odoo.debug ? `${field.string} (${field.name})` : field.string || field.name,\n        value: field.name,\n    }));\n}\n\nexport function getStudioNoFetchFields(_fieldNodes) {\n    const fieldNames = [];\n    const fieldNodes = [];\n    Object.entries(_fieldNodes)\n        .filter(([fNode, field]) => field.attrs && field.attrs.studio_no_fetch)\n        .forEach(([fNode, field]) => {\n            fieldNames.push(field.name);\n            fieldNodes.push(fNode);\n        });\n    return {\n        fieldNames,\n        fieldNodes,\n    };\n}\n\nexport function useModelConfigFetchInvisible(model) {\n    function fixActiveFields(activeFields) {\n        const stack = [activeFields];\n        while (stack.length) {\n            const activeFields = stack.pop();\n            for (const activeField of Object.values(activeFields)) {\n                if (\"related\" in activeField) {\n                    stack.push(activeField.related.activeFields);\n                }\n                delete activeField.invisible;\n            }\n        }\n        return activeFields;\n    }\n\n    const load = model.load;\n    model.load = (...args) => {\n        fixActiveFields(model.config.activeFields);\n        return load.call(model, ...args);\n    };\n}\n\nexport function getCurrencyField(fieldsGet) {\n    const field = Object.entries(fieldsGet).find(([fName, fInfo]) => {\n        return fInfo.type === \"many2one\" && fInfo.relation === \"res.currency\";\n    });\n    if (field) {\n        return field[0];\n    }\n}\n", "/** @odoo-module */\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { isComponentNode, appendAttr } from \"@web/views/view_compiler\";\n\nconst nodeWeak = new WeakMap();\n\nexport function countPreviousSiblings(node) {\n    const countXpath = `count(preceding-sibling::${node.tagName})`;\n    return node.ownerDocument.evaluate(countXpath, node, null, XPathResult.NUMBER_TYPE).numberValue;\n}\n\nexport function computeXpath(node, upperBoundSelector = \"form\") {\n    if (nodeWeak.has(node)) {\n        return nodeWeak.get(node);\n    }\n    const tagName = node.tagName;\n    const count = countPreviousSiblings(node) + 1;\n\n    let xpath = `${tagName}[${count}]`;\n    const parent = node.parentElement;\n    if (!node.matches(upperBoundSelector)) {\n        const parentXpath = computeXpath(parent, upperBoundSelector);\n        xpath = `${parentXpath}/${xpath}`;\n    } else {\n        xpath = `/${xpath}`;\n    }\n    nodeWeak.set(node, xpath);\n    return xpath;\n}\n\nexport function getNodeAttributes(node) {\n    const attrs = {};\n    for (const att of node.getAttributeNames()) {\n        if (att === \"options\") {\n            attrs[att] = evaluateExpr(node.getAttribute(att));\n            continue;\n        }\n        attrs[att] = node.getAttribute(att);\n    }\n    return attrs;\n}\n\nfunction getXpathNodes(xpathResult) {\n    const nodes = [];\n    let res;\n    while ((res = xpathResult.iterateNext())) {\n        nodes.push(res);\n    }\n    return nodes;\n}\n\nexport function getNodesFromXpath(xpath, xml) {\n    const owner = \"evaluate\" in xml ? xml : xml.ownerDocument;\n    const xpathResult = owner.evaluate(xpath, xml, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);\n    return getXpathNodes(xpathResult);\n}\n\nconst parser = new DOMParser();\nexport const parseStringToXml = (str) => {\n    return parser.parseFromString(str, \"text/xml\");\n};\n\nconst serializer = new XMLSerializer();\nexport const serializeXmlToString = (xml) => {\n    return serializer.serializeToString(xml);\n};\n\n// This function should be used in Compilers to apply the \"invisible\" modifiers on\n// the compiled templates's nodes\nexport function applyInvisible(invisible, compiled, params) {\n    // Just return the node if it is always Visible\n    if (!invisible || invisible === \"False\" || invisible === \"0\") {\n        return compiled;\n    }\n\n    let isVisileExpr;\n    // If invisible is dynamic, pass a props or apply the studio class.\n    if (invisible !== \"True\" && invisible !== \"1\") {\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        isVisileExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n            invisible\n        )},${recordExpr}.evalContextWithVirtualIds)`;\n        if (isComponentNode(compiled)) {\n            compiled.setAttribute(\"studioIsVisible\", isVisileExpr);\n        } else {\n            appendAttr(compiled, \"class\", `o_web_studio_show_invisible:!${isVisileExpr}`);\n        }\n    } else {\n        if (isComponentNode(compiled)) {\n            compiled.setAttribute(\"studioIsVisible\", \"false\");\n        } else {\n            appendAttr(compiled, \"class\", `o_web_studio_show_invisible:true`);\n        }\n    }\n\n    // Finally, put a t-if on the node that accounts for the parameter in the config.\n    const studioShowExpr = `__comp__.viewEditorModel.showInvisible`;\n    isVisileExpr = isVisileExpr ? `(${isVisileExpr} or ${studioShowExpr})` : studioShowExpr;\n    if (compiled.hasAttribute(\"t-if\")) {\n        const formerTif = compiled.getAttribute(\"t-if\");\n        isVisileExpr = `( ${formerTif} ) and ${isVisileExpr}`;\n    }\n    compiled.setAttribute(\"t-if\", isVisileExpr);\n    return compiled;\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { RecordSelector } from \"@web/core/record_selectors/record_selector\";\n\nexport class DialogAddNewButton extends Component {\n    static template = `web_studio.DialogNewButtonStatusBar`;\n    static components = {\n        AutoComplete,\n        Dialog,\n        Dropdown,\n        DropdownItem,\n        RecordSelector,\n    };\n    static props = {\n        model: { type: String },\n        onConfirm: { type: Function },\n        close: { type: Function },\n    };\n    setup() {\n        this.state = useState({\n            action: \"\",\n            button_type: \"\",\n            actionId: false,\n            methodId: \"\",\n            methodList: [],\n            error: \"\",\n            methodChecked: false,\n        });\n    }\n\n    get multiRecordSelectorProps() {\n        return {\n            resModel: \"ir.actions.actions\",\n            update: (resId) => {\n                this.state.actionId = resId;\n            },\n            resId: this.state.actionId,\n            domain: [[\"binding_model_id\", \"=\", this.props.model]],\n        };\n    }\n\n    get checkValidity() {\n        if (this.state.label?.length > 0) {\n            if (this.state.button_type === \"action\" && this.state.actionId) {\n                return false;\n            } else if (\n                this.state.methodChecked &&\n                this.state.button_type === \"object\" &&\n                this.state.methodId?.length > 0 &&\n                this.state.error?.length === 0\n            ) {\n                return false;\n            } else {\n                return true;\n            }\n        } else {\n            return true;\n        }\n    }\n    onChange() {\n        this.state.actionId = false;\n        this.state.methodId = null;\n    }\n    onConfirm() {\n        this.props.onConfirm(this.state);\n        this.props.close();\n    }\n    onCancel() {\n        this.props.close();\n    }\n    async checkMethod() {\n        this.state.error = \"\";\n        this.state.methodChecked = false;\n        if (this.state.methodId?.length > 0) {\n            if (this.state.methodId.startsWith(\"_\")) {\n                this.state.error = _t(\"The method %s is private.\", this.state.methodId);\n            } else {\n                try {\n                    await rpc(\"/web_studio/check_method\", {\n                        model_name: this.props.model,\n                        method_name: this.state.methodId,\n                    });\n                } catch (error) {\n                    if (error?.data?.message?.length > 0) {\n                        this.state.error = error.data.message;\n                    }\n                }\n                this.state.methodChecked = true;\n            }\n        }\n    }\n}\n\nexport class AddButtonAction extends Component {\n    static props = {};\n    static template = `web_studio.AddButtonAction`;\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n    onClick() {\n        this.addDialog(DialogAddNewButton, {\n            model: this.env.viewEditorModel.resModel,\n            onConfirm: (state) => {\n                const viewEditorModel = this.env.viewEditorModel;\n                const arch = viewEditorModel.xmlDoc;\n                const findHeader = arch.firstChild.querySelector(\":scope > header\");\n                if (!findHeader) {\n                    viewEditorModel.pushOperation({\n                        type: \"statusbar\",\n                        view_id: this.env.viewEditorModel.view.id,\n                    });\n                }\n                viewEditorModel.doOperation({\n                    type: \"add_button_action\",\n                    button_type: state.button_type,\n                    actionId: state.actionId,\n                    methodId: state.methodId,\n                    label: state.label,\n                });\n            },\n        });\n    }\n}\n", "/** @odoo-module */\nimport { Component, useState, xml } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { useDialogConfirmation } from \"@web_studio/client_action/utils\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { SelectionContentDialog } from \"@web_studio/client_action/view_editor/interactive_editor/field_configuration/selection_content_dialog\";\nimport { RecordSelector } from \"@web/core/record_selectors/record_selector\";\n\nexport class SelectionValuesEditor extends Component {\n    static components = {\n        SelectionContentDialog,\n    };\n    static props = {\n        configurationModel: { type: Object },\n        confirm: { type: Function },\n        cancel: { type: Function },\n    };\n    static template = \"web_studio.SelectionValuesEditor\";\n    static Model = class SelectionValuesModel {\n        constructor() {\n            this.selection = \"[]\";\n        }\n        get isValid() {\n            return true;\n        }\n    };\n    get selection() {\n        return JSON.parse(this.props.configurationModel.selection);\n    }\n    onConfirm(choices) {\n        this.props.configurationModel.selection = JSON.stringify(choices);\n        this.props.confirm();\n    }\n}\n\nexport class RelationalFieldConfigurator extends Component {\n    static template = \"web_studio.RelationalFieldConfigurator\";\n    static components = { RecordSelector };\n    static props = {\n        configurationModel: { type: Object },\n        resModel: { type: String },\n        fieldType: { type: String },\n    };\n    static Model = class RelationalFieldModel {\n        constructor() {\n            this.relationId = false;\n        }\n        get isValid() {\n            return !!this.relationId;\n        }\n    };\n\n    setup() {\n        this.state = useState(this.props.configurationModel);\n    }\n\n    get valueSelectorProps() {\n        if (this.props.fieldType === \"one2many\") {\n            return {\n                resModel: \"ir.model.fields\",\n                domain: [\n                    [\"relation\", \"=\", this.props.resModel],\n                    [\"ttype\", \"=\", \"many2one\"],\n                    [\"model_id.abstract\", \"=\", false],\n                    [\"store\", \"=\", true],\n                ],\n                resId: this.state.relationId,\n                update: (resId) => {\n                    this.state.relationId = resId;\n                },\n            };\n        }\n        return {\n            resModel: \"ir.model\",\n            domain: [\n                [\"transient\", \"=\", false],\n                [\"abstract\", \"=\", false],\n            ],\n            resId: this.state.relationId,\n            update: (resId) => {\n                this.state.relationId = resId;\n            },\n        };\n    }\n}\n\nclass RelatedChainBuilderModel {\n    static services = [\"field\", \"dialog\"];\n\n    constructor({ services, props }) {\n        this.services = services;\n        this.relatedParams = {};\n        this.fieldInfo = { resModel: props.resModel, fieldDef: null };\n        this.resModel = props.resModel;\n    }\n\n    get isValid() {\n        return !!this.relatedParams.related;\n    }\n\n    getRelatedFieldDescription(resModel, lastField) {\n        const fieldType = lastField.type;\n        const relatedDescription = {\n            readonly: true,\n            copy: false,\n            string: lastField.string,\n            type: fieldType,\n            store: false,\n        };\n\n        if ([\"many2one\", \"many2many\", \"one2many\"].includes(fieldType)) {\n            relatedDescription.relation = lastField.relation;\n        }\n        if ([\"one2many\", \"many2many\"].includes(fieldType)) {\n            relatedDescription.relational_model = resModel;\n        }\n        if (fieldType === \"selection\") {\n            relatedDescription.selection = lastField.selection;\n        }\n        return relatedDescription;\n    }\n\n    async confirm() {\n        const relatedDescription = this.getRelatedFieldDescription(\n            this.fieldInfo.resModel,\n            this.fieldInfo.fieldDef\n        );\n        Object.assign(this.relatedParams, relatedDescription);\n        return true;\n    }\n}\n\nexport class RelatedChainBuilder extends Component {\n    static template = xml`<ModelFieldSelector resModel=\"props.resModel\" path=\"fieldChain\" readonly=\"false\" filter.bind=\"filter\" update.bind=\"updateChain\" />`;\n    static components = { ModelFieldSelector };\n    static props = {\n        resModel: { type: String },\n        configurationModel: { type: Object },\n    };\n    static Model = RelatedChainBuilderModel;\n\n    setup() {\n        this.state = useState(this.props.configurationModel);\n        this.relatedParams.related = \"\";\n    }\n\n    get relatedParams() {\n        return this.state.relatedParams;\n    }\n\n    get fieldChain() {\n        return this.relatedParams.related;\n    }\n\n    filter(fieldDef, path) {\n        return fieldDef.type !== \"properties\";\n    }\n\n    async updateChain(path, fieldInfo) {\n        this.relatedParams.related = path;\n        this.state.fieldInfo = fieldInfo;\n    }\n}\n\nfunction useConfiguratorModel(Model, props) {\n    const services = Object.fromEntries(\n        (Model.services || []).map((servName) => {\n            let serv;\n            if (servName === \"dialog\") {\n                serv = { add: useOwnedDialogs() };\n            } else {\n                serv = useService(servName);\n            }\n            return [servName, serv];\n        })\n    );\n\n    const model = new Model({ services, props });\n    return useState(model);\n}\n\nexport class FieldConfigurationDialog extends Component {\n    static props = {\n        confirm: { type: Function },\n        cancel: { type: Function },\n        close: { type: Function },\n        Component: { type: Function },\n        componentProps: { type: Object, optional: true },\n        fieldType: { type: String, optional: true },\n        isDialog: { type: Boolean, optional: true },\n        title: { type: String, optional: true },\n        size: { type: String, optional: true },\n    };\n    static template = \"web_studio.FieldConfigurationDialog\";\n    static components = { Dialog };\n\n    setup() {\n        const { confirm, cancel } = useDialogConfirmation({\n            confirm: async () => {\n                let confirmValues = false;\n                if (!this.configurationModel.isValid) {\n                    return false;\n                }\n                if (this.configurationModel.confirm) {\n                    const res = await this.configurationModel.confirm();\n                    if (res || res === undefined) {\n                        confirmValues = this.configurationModel;\n                    }\n                } else {\n                    confirmValues = this.configurationModel;\n                }\n                return this.props.confirm(confirmValues);\n            },\n            cancel: () => this.props.cancel(),\n        });\n        this.confirm = confirm;\n        this.cancel = cancel;\n        this.configurationModel = useConfiguratorModel(\n            this.Component.Model,\n            this.props.componentProps\n        );\n    }\n\n    get title() {\n        if (this.props.title) {\n            return this.props.title;\n        }\n        if (this.props.fieldType) {\n            return _t(\"Field properties: %s\", this.props.fieldType);\n        }\n        return \"\";\n    }\n\n    get Component() {\n        return this.props.Component;\n    }\n\n    get canConfirm() {\n        return this.configurationModel.isValid;\n    }\n}\n\nexport class FilterConfiguration extends Component {\n    static components = { DomainSelector };\n    static template = \"web_studio.FilterConfiguration\";\n    static props = {\n        resModel: { type: String },\n        configurationModel: { type: Object },\n    };\n    static Model = class FilterConfigurationModel {\n        constructor() {\n            this.filterLabel = \"\";\n            this.domain = \"[]\";\n        }\n\n        get isValid() {\n            return !!this.filterLabel;\n        }\n    };\n\n    setup() {\n        this.state = useState(this.props.configurationModel);\n    }\n\n    get domainSelectorProps() {\n        return {\n            resModel: this.props.resModel,\n            readonly: false,\n            domain: this.state.domain,\n            update: (domainStr) => {\n                this.state.domain = domainStr;\n            },\n            isDebugMode: !!this.env.debug,\n        };\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class SelectionContentDialog extends Component {\n    static components = {\n        Dialog,\n    };\n    static defaultProps = {\n        defaultChoices: [],\n    };\n    static props = {\n        defaultChoices: { type: Array, optional: true },\n        onConfirm: { type: Function },\n        close: { type: Function },\n    };\n    static template = \"web_studio.SelectionContentDialog\";\n\n    setup() {\n        this.state = useState({\n            choices: this.props.defaultChoices,\n        });\n        this.localState = useState({\n            _newItem: [],\n            editedItem: null,\n        });\n\n        const itemsList = useRef(\"itemsList\");\n        useSortable({\n            enable: () => !this.editedItem,\n            handle: \".o-draggable-handle\",\n            ref: itemsList,\n            elements: \".o-draggable\",\n            cursor: \"move\",\n            onDrop: (params) => this.resequenceItems(params),\n        });\n\n        this.oldValue = new WeakMap();\n    }\n\n    getSelectionFromItem(item) {\n        if (item.id === \"new\") {\n            return this.localState._newItem;\n        }\n        return this.selection[item.id];\n    }\n\n    get selection() {\n        return this.state.choices;\n    }\n\n    set selection(items) {\n        this.state.choices = items;\n    }\n\n    selectionToItem(selection, params = {}) {\n        return Object.assign(\n            {\n                id: \"new\",\n                key: selection[0],\n                name: selection[0],\n                label: selection[1],\n                isDraggable: false,\n                isRemovable: false,\n                isInEdition: false,\n            },\n            params\n        );\n    }\n\n    get selectionToItems() {\n        const inEdition = !!this.editedItem;\n        return this.selection.map((sel, index) => {\n            return this.selectionToItem(sel, {\n                id: index,\n                key: inEdition ? index : sel[0],\n                isInEdition:\n                    this.editedItem?.id === this.selection.indexOf(sel) && !this.shouldFullEdit,\n                isDraggable: !inEdition,\n                isRemovable: !inEdition,\n            });\n        });\n    }\n\n    get newItem() {\n        return this.selectionToItem(this.localState._newItem, { isInEdition: true, id: \"new\" });\n    }\n\n    get editedItem() {\n        return this.localState.editedItem;\n    }\n\n    get shouldFullEdit() {\n        return Boolean(this.env.debug);\n    }\n\n    ensureUnique(item) {\n        const value = item[0];\n        if (!value) {\n            return false;\n        }\n\n        const otherElements = this.selection.filter((i) => i !== item);\n        if (otherElements.some((i) => i[0] === value)) {\n            return false;\n        }\n        return true;\n    }\n\n    setItemValue(item, value) {\n        if (item.id !== \"new\" && item.id !== this.editedItem.id) {\n            return;\n        }\n        const isEditingLabel = item.id !== \"new\";\n        item = this.getSelectionFromItem(item);\n        item[0] = isEditingLabel ? this.editedItem.name : value;\n        item[1] = value;\n    }\n\n    addItem(item) {\n        if (!this.ensureUnique(item)) {\n            return;\n        }\n        this.selection.push(item);\n        this.localState._newItem = [];\n    }\n\n    removeItem(item) {\n        this.selection = this.selection.filter((i) => i[0] !== item.name);\n    }\n\n    editItem(item) {\n        const selItem = this.getSelectionFromItem(item);\n        if (item.id === \"new\") {\n            return this.addItem(selItem);\n        }\n        if (this.editedItem?.id === item.id) {\n            if (!this.ensureUnique(selItem)) {\n                return;\n            }\n            this.localState.editedItem = null;\n            this.oldValue.delete(selItem);\n            return;\n        }\n        this.oldValue.set(selItem, [...selItem]);\n        this.localState.editedItem = item;\n    }\n\n    discardItemChanges(item) {\n        if (item.id === \"new\") {\n            return this.setItemValue(item, \"\");\n        }\n        const selItem = this.getSelectionFromItem(item);\n        const oldValue = this.oldValue.get(selItem);\n        selItem[0] = oldValue[0];\n        selItem[1] = oldValue[1];\n        this.localState.editedItem = null;\n    }\n\n    resequenceItems(params) {\n        const { previous, next, element } = params;\n        const itemId = parseInt(element.dataset.itemId);\n\n        let items = this.selection;\n        const item = items[itemId];\n        items = items.filter((i) => i !== item);\n\n        let toIndex;\n        if (previous) {\n            toIndex = parseInt(previous.dataset.itemId) + 1;\n        } else if (next) {\n            toIndex = parseInt(next.dataset.itemId);\n        }\n        items.splice(toIndex, 0, item);\n        this.selection = items;\n    }\n\n    async onConfirm() {\n        if (this.newItem.name?.length) {\n            this.editItem(this.newItem);\n        }\n        await this.props.onConfirm(this.selection);\n        this.props.close();\n    }\n\n    onKeyPressed(item, key) {\n        if (key === \"Enter\") {\n            this.editItem(item);\n        }\n    }\n}\n", "import { Component, toRaw } from \"@odoo/owl\";\n\nimport { closest, touching } from \"@web/core/utils/ui\";\nimport { useDraggable } from \"@web/core/utils/draggable\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    isToXpathEquivalentFromXpath,\n    cleanHooks,\n    getActiveHook,\n    getCurrencyField,\n    getHooks,\n    hookPositionTolerance,\n    randomName,\n} from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    FieldConfigurationDialog,\n    SelectionValuesEditor,\n    RelationalFieldConfigurator,\n    RelatedChainBuilder,\n} from \"@web_studio/client_action/view_editor/interactive_editor/field_configuration/field_configuration\";\nimport {\n    getNodesFromXpath,\n    countPreviousSiblings,\n} from \"@web_studio/client_action/view_editor/editors/xml_utils\";\nimport { DefaultViewSidebar } from \"@web_studio/client_action/view_editor/default_view_sidebar/default_view_sidebar\";\n\nconst NO_M2O_AVAILABLE = _t(`\n    There are no many2one fields related to the current model.\n    To create a one2many field on the current model, you must first create its many2one counterpart on the model you want to relate to.\n`);\n\nfunction copyElementOnDrag() {\n    let element;\n    let copy;\n\n    function clone(_element) {\n        element = _element;\n        copy = element.cloneNode(true);\n    }\n\n    function insert() {\n        if (element) {\n            element.insertAdjacentElement(\"beforebegin\", copy);\n        }\n    }\n\n    function clean() {\n        if (copy) {\n            copy.remove();\n        }\n        copy = null;\n        element = null;\n    }\n\n    return { clone, insert, clean };\n}\n\nexport class InteractiveEditor extends Component {\n    static template = \"web_studio.InteractiveEditor\";\n    static components = {};\n    static props = {\n        editor: true,\n        slots: { type: Object },\n        editorContainerRef: { type: Object },\n        rendererRef: { type: Object },\n    };\n\n    setup() {\n        this.defaultSidebar = DefaultViewSidebar;\n\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.addDialog = useOwnedDialogs();\n        this.notification = useService(\"notification\");\n        /* DagDrop: from sidebar to View, and within the view */\n        const getNearestHook = this.getNearestHook.bind(this);\n        // Those are fine because editor defines the t-key\n        const prepareForDrag = this.props.editor.prepareForDrag;\n        const isValidHook = this.props.editor.isValidHook || (() => true);\n        this.addViewStructure = this.props.editor.addViewStructure;\n        const styleNearestHook =\n            this.props.editor.styleNearestHook ||\n            ((ref, hook) => {\n                hook.classList.add(\"o_web_studio_nearest_hook\");\n            });\n\n        function removeBootStrapClasses(element) {\n            const bootstrapClasses = Array.from(element.classList).filter(\n                (c) => c.startsWith(\"position-\") || c.startsWith(\"w-\") || c.startsWith(\"h-\")\n            );\n            if (!bootstrapClasses.length) {\n                return () => {};\n            }\n            element.classList.remove(...bootstrapClasses);\n            return () => {\n                element.classList.add(...bootstrapClasses);\n            };\n        }\n\n        let cleanUps;\n        const copyOnDrag = copyElementOnDrag();\n        useDraggable({\n            ref: this.props.editorContainerRef,\n            elements: \".o-draggable\",\n            onWillStartDrag: ({ element }) => {\n                cleanUps = [];\n                if (element.closest(\".o_web_studio_component\")) {\n                    copyOnDrag.clone(element);\n                }\n            },\n            onDragStart: ({ element }) => {\n                cleanUps.push(removeBootStrapClasses(element));\n                copyOnDrag.insert();\n                if (prepareForDrag) {\n                    cleanUps.push(\n                        prepareForDrag({\n                            element,\n                            viewEditorModel: this.viewEditorModel,\n                            ref: this.props.editorContainerRef,\n                        })\n                    );\n                }\n            },\n            onDrag: ({ x, y, element }) => {\n                cleanHooks(this.viewRef.el);\n                element.classList.remove(\"o-draggable--drop-ready\");\n                const hook = getNearestHook(element, { x, y });\n                if (!hook) {\n                    return;\n                }\n                if (!isValidHook({ hook, element, viewEditorModel: this.viewEditorModel })) {\n                    return;\n                }\n                styleNearestHook(this.props.rendererRef, hook);\n                element.classList.add(\"o-draggable--drop-ready\");\n            },\n            onDrop: ({ element }) => {\n                const targetHook = getActiveHook(this.viewRef.el);\n                if (!targetHook) {\n                    return;\n                }\n                const { xpath, position, type, infos } = targetHook.dataset;\n                const droppedData = element.dataset;\n\n                const isNew = element.classList.contains(\"o_web_studio_component\");\n                const structure = isNew ? droppedData.structure : \"field\"; // only fields can be moved\n\n                if (isNew) {\n                    this.addStructure(structure, droppedData.drop, {\n                        xpath,\n                        position,\n                        type,\n                        infos,\n                    });\n                } else {\n                    this.moveStructure(structure, droppedData, { xpath, position });\n                }\n            },\n            onDragEnd: ({ element }) => {\n                cleanHooks(this.viewRef.el);\n                if (cleanUps) {\n                    cleanUps.forEach((c) => c());\n                    cleanUps = null;\n                }\n                copyOnDrag.clean();\n            },\n        });\n\n        this.applyAutoClick = () => {\n            if (!this.autoClick) {\n                return;\n            }\n\n            const { targetInfo, tag, attrs } = this.autoClick;\n\n            // First step: locate node in new arch\n            let xpathToClick = targetInfo.xpath;\n            if (tag) {\n                // We are trying to select a new node of which targetInfo could be its parent\n                if (targetInfo.position !== \"inside\") {\n                    xpathToClick = xpathToClick.split(\"/\").slice(0, -1).join(\"/\");\n                }\n\n                const attrForXpath = Object.entries(attrs)\n                    .filter(([, value]) => !!value)\n                    .map(([attName, value]) => {\n                        return `@${attName}='${value}'`;\n                    })\n                    .join(\" and \");\n                const nodeXpath = `${tag}[${attrForXpath}]`;\n                const fullXpath = `${xpathToClick}/${nodeXpath}`;\n\n                const nodes = getNodesFromXpath(fullXpath, toRaw(this.viewEditorModel).xmlDoc);\n                this.autoClick = null; // Early reset of that variable\n                if (nodes.length !== 1) {\n                    return;\n                }\n                const atPosition = countPreviousSiblings(nodes[0]) + 1;\n                xpathToClick = `${xpathToClick}/${tag}[${atPosition}]`;\n            }\n\n            // Second step: locate corresponding dom element\n            const domEl = this.props.rendererRef.el.querySelector(\n                `[data-studio-xpath='${xpathToClick}'], [studioxpath='${xpathToClick}']`\n            );\n            if (domEl) {\n                domEl.click();\n            }\n        };\n    }\n\n    get viewEditorModel() {\n        return this.env.viewEditorModel;\n    }\n\n    get viewRef() {\n        return this.viewEditorModel.viewRef;\n    }\n\n    getNearestHook(draggedEl, { x, y }) {\n        const viewRefEl = this.viewRef.el;\n        cleanHooks(viewRefEl);\n\n        const mouseToleranceRect = {\n            x: x - hookPositionTolerance,\n            y: y - hookPositionTolerance,\n            width: hookPositionTolerance * 2,\n            height: hookPositionTolerance * 2,\n        };\n\n        const touchingEls = touching(getHooks(viewRefEl), mouseToleranceRect);\n        const closestHookEl = closest(touchingEls, { x, y });\n\n        return closestHookEl;\n    }\n\n    openViewInForm() {\n        return this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"ir.ui.view\",\n                res_id: this.env.viewEditorModel.mainView.id,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            },\n            { clearBreadcrumbs: true }\n        );\n    }\n\n    openDefaultValues() {\n        const resModel = this.env.viewEditorModel.resModel;\n        this.action.doAction(\n            {\n                name: _t(\"Default Values\"),\n                type: \"ir.actions.act_window\",\n                res_model: \"ir.default\",\n                target: \"current\",\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"field_id.model\", \"=\", resModel]],\n            },\n            { clearBreadcrumbs: true }\n        );\n    }\n\n    setAutoClick(targetInfo, nodeDescr) {\n        if (!targetInfo) {\n            this.autoClick = null;\n            return;\n        }\n        if (targetInfo && !nodeDescr) {\n            this.autoClick = {\n                targetInfo,\n            };\n            return;\n        }\n        let nameAttr = nodeDescr.attrs?.name;\n        if (nodeDescr.tag === \"field\" && !nameAttr) {\n            nameAttr = nodeDescr.field_description.name;\n        } else if (!nameAttr) {\n            this.autoClick = {\n                targetInfo,\n            };\n            return;\n        }\n\n        this.autoClick = {\n            targetInfo,\n            tag: nodeDescr.tag,\n            attrs: { name: nameAttr },\n        };\n    }\n\n    async addField(droppedData) {\n        const data = JSON.parse(droppedData);\n        const isExistingField = \"fieldName\" in data;\n\n        let newNode;\n        if (!isExistingField) {\n            newNode = await this.getNewFieldNode(data);\n        } else {\n            newNode = {\n                tag: \"field\",\n                attrs: { name: data.fieldName },\n            };\n\n            const field = this.viewEditorModel.fields[data.fieldName];\n            if (field.type === \"monetary\") {\n                this.setCurrencyInfos(newNode.attrs);\n            }\n        }\n        if (!newNode) {\n            return;\n        }\n        if (!isExistingField) {\n            this.viewEditorModel.setRenameableField(newNode.field_description?.name, true);\n        }\n\n        if (this.viewEditorModel.viewType === \"kanban\") {\n            newNode.attrs.display = \"full\";\n        }\n\n        if (this.viewEditorModel.viewType === \"list\") {\n            newNode.attrs.optional = \"show\";\n        }\n\n        return {\n            node: newNode,\n        };\n    }\n\n    async addStructure(structure, droppedData, targetInfo) {\n        let _operation;\n        if (this.addViewStructure) {\n            _operation = await this.addViewStructure(structure, {\n                droppedData,\n                targetInfo,\n                addDialog: this.addDialog.bind(this),\n            });\n        }\n        if (!_operation && structure === \"field\") {\n            _operation = await this.addField(droppedData);\n        }\n        if (!_operation) {\n            return;\n        }\n        const operation = {\n            target:\n                _operation?.target ||\n                (targetInfo.xpath\n                    ? this.viewEditorModel.getFullTarget(targetInfo.xpath)\n                    : undefined),\n            position: targetInfo.position,\n            type: \"add\",\n            ..._operation,\n        };\n        this.setAutoClick(targetInfo, operation.node);\n        return this.viewEditorModel.doOperation(operation);\n    }\n\n    async getNewFieldNode(data) {\n        const string = _t(\"New %s\", data.string);\n\n        const newNode = {\n            field_description: {\n                field_description: string,\n                name: randomName(`x_studio_${data.fieldType}_field`),\n                type: data.fieldType,\n                model_name: this.viewEditorModel.resModel,\n                special: data.special,\n            },\n            tag: \"field\",\n            attrs: { widget: data.widget },\n        };\n\n        if (data.special === \"lines\") {\n            return newNode;\n        }\n\n        const fieldType = data.fieldType;\n        if (fieldType === \"selection\" && data.widget === \"priority\") {\n            // should not be translated at the creation\n            newNode.field_description.selection = [\n                [\"0\", \"Normal\"],\n                [\"1\", \"Low\"],\n                [\"2\", \"High\"],\n                [\"3\", \"Very High\"],\n            ];\n            return newNode;\n        }\n\n        if ([\"selection\", \"one2many\", \"many2one\", \"many2many\", \"related\"].includes(fieldType)) {\n            if (fieldType === \"one2many\") {\n                const count = await this.orm.searchCount(\"ir.model.fields\", [\n                    [\"relation\", \"=\", this.viewEditorModel.resModel],\n                    [\"ttype\", \"=\", \"many2one\"],\n                    [\"store\", \"=\", true],\n                ]);\n                if (!count) {\n                    this.addDialog(ConfirmationDialog, {\n                        title: _t(\"No related many2one fields found\"),\n                        body: NO_M2O_AVAILABLE,\n                        confirm: async () => {},\n                    });\n                    return;\n                }\n            }\n\n            const fieldParams = await this.openFieldConfiguration(fieldType);\n            if (!fieldParams) {\n                return;\n            }\n            if (fieldType === \"selection\") {\n                newNode.field_description.selection = fieldParams.selection;\n            }\n            if (fieldType === \"one2many\") {\n                newNode.field_description.relation_field_id = fieldParams.relationId;\n            }\n            if (fieldType === \"many2many\" || fieldType === \"many2one\") {\n                newNode.field_description.relation_id = fieldParams.relationId;\n            }\n            if (fieldType === \"related\") {\n                Object.assign(newNode.field_description, fieldParams.relatedParams);\n                if (!newNode.field_description.related) {\n                    delete newNode.field_description.related;\n                }\n            }\n        }\n\n        if (\n            fieldType === \"monetary\" ||\n            (fieldType === \"related\" && newNode.field_description?.type === \"monetary\")\n        ) {\n            this.setCurrencyInfos(newNode.field_description);\n        }\n\n        if (fieldType === \"integer\") {\n            newNode.field_description.default_value = \"0\";\n        }\n\n        return newNode;\n    }\n\n    openFieldConfiguration(fieldType) {\n        let dialogProps;\n        if (fieldType === \"selection\") {\n            dialogProps = {\n                Component: SelectionValuesEditor,\n                isDialog: true,\n            };\n        } else if ([\"one2many\", \"many2many\", \"many2one\"].includes(fieldType)) {\n            dialogProps = {\n                Component: RelationalFieldConfigurator,\n                componentProps: { fieldType, resModel: this.viewEditorModel.resModel },\n            };\n        } else if (fieldType === \"related\") {\n            dialogProps = {\n                Component: RelatedChainBuilder,\n                componentProps: {\n                    resModel: this.viewEditorModel.resModel,\n                },\n            };\n        }\n\n        const fieldParams = new Promise((resolve, reject) => {\n            this.addDialog(FieldConfigurationDialog, {\n                fieldType,\n                confirm: async (params) => {\n                    resolve(params);\n                },\n                cancel: () => resolve(false),\n                ...dialogProps,\n            });\n        });\n        return fieldParams;\n    }\n\n    moveStructure(structure, droppedData, targetInfo) {\n        if (structure !== \"field\") {\n            throw Error(\"Moving anything else than a field is not supported\");\n        }\n\n        if (\n            isToXpathEquivalentFromXpath(\n                targetInfo.position,\n                targetInfo.xpath,\n                droppedData.studioXpath\n            )\n        ) {\n            return;\n        }\n\n        const operation = {\n            type: \"move\",\n            node: this.viewEditorModel.getFullTarget(droppedData.studioXpath),\n            target: this.viewEditorModel.getFullTarget(targetInfo.xpath),\n            position: targetInfo.position,\n        };\n        const subViewXpath = this.viewEditorModel.getSubviewXpath();\n        if (subViewXpath) {\n            operation.node.subview_xpath = subViewXpath;\n        }\n\n        if (this.viewEditorModel.activeNodeXpath === droppedData.studioXpath) {\n            this.setAutoClick(targetInfo, operation.node);\n        }\n        this.viewEditorModel.doOperation(operation);\n    }\n\n    setCurrencyInfos(object) {\n        const currencyField = getCurrencyField(this.viewEditorModel.fields);\n        if (currencyField) {\n            object.currency_field = currencyField;\n            object.currency_in_view = this.viewEditorModel.fieldsInArch.includes(currencyField);\n        }\n    }\n}\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\nimport { onWillStart, useState, onWillUpdateProps, Component } from \"@odoo/owl\";\n\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nconst tabsDisplay = {\n    new: {\n        class: \"o_web_studio_new px-2\",\n        title: _t(\"Add\"),\n    },\n    view: {\n        class: \"o_web_studio_view px-2\",\n        title: _t(\"View\"),\n    },\n    properties: {\n        class: \"o_web_studio_properties px-2\",\n        title: _t(\"Properties\"),\n    },\n};\n\nexport class InteractiveEditorSidebar extends Component {\n    static components = { Notebook };\n    static template = \"web_studio.ViewEditor.InteractiveEditorSidebar\";\n    static props = {\n        slots: { type: Object },\n    };\n\n    setup() {\n        this.editorModel = useState(this.env.viewEditorModel);\n        this.tabsDisplay = tabsDisplay;\n        useBus(this.editorModel.bus, \"error\", () => this.render(true));\n\n        this._defaultTab = this.computeDefaultTab(this.props);\n        this.editorModel.sidebarTab = this._defaultTab;\n\n        onWillStart(() => {\n            this.editorModel.resetSidebar();\n        });\n        onWillUpdateProps(() => {\n            // This component takes slots: it is always re-rendered\n            const editorModel = this.editorModel;\n            if (editorModel.sidebarTab === \"properties\" && !editorModel.activeNode) {\n                editorModel.resetSidebar();\n            }\n        });\n    }\n\n    get icons() {\n        return {\n            new: \"fa-plus\",\n            view: \"fa-television\",\n            properties: \"fa-server\",\n        };\n    }\n\n    computeDefaultTab(props) {\n        const slots = props.slots;\n        const defaults = Object.keys(slots).filter((s) => slots[s].isDefault);\n        if (defaults.length) {\n            return defaults[0];\n        }\n        return \"new\" in slots ? \"new\" : \"view\";\n    }\n\n    get defaultTab() {\n        return this.editorModel.sidebarTab || this._defaultTab;\n    }\n\n    onTabClicked(tab) {\n        if (tab !== \"properties\") {\n            this.editorModel.resetSidebar(tab);\n        }\n        this.editorModel.sidebarTab = tab;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\n\nexport class ClassAttribute extends Component {\n    static template = \"web_studio.ViewEditor.ClassAttribute\";\n    static components = {\n        Property,\n    };\n    static props = {\n        value: { type: String, optional: true },\n        onChange: { type: Function },\n    };\n    get tooltip() {\n        return _t(\n            \"Use Bootstrap or any other custom classes to customize the style and the display of the element.\"\n        );\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps, useState, toRaw } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { SelectionContentDialog } from \"@web_studio/client_action/view_editor/interactive_editor/field_configuration/selection_content_dialog\";\nimport { TypeWidgetProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/type_widget_properties/type_widget_properties\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ClassAttribute } from \"../class_attribute/class_attribute\";\nimport { ModifiersProperties } from \"../modifiers/modifiers_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nclass TechnicalName extends Component {\n    static props = {\n        node: { type: Object },\n    };\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Field.TechnicalName\";\n    static components = { Property };\n\n    setup() {\n        this.renameField = (value) => {\n            return this.env.viewEditorModel.renameField(\n                this.props.node.attrs.name,\n                `x_studio_${value}`,\n                { autoUnique: false }\n            );\n        };\n    }\n\n    get canEdit() {\n        return (\n            this.env.debug && this.env.viewEditorModel.isFieldRenameable(this.props.node.attrs.name)\n        );\n    }\n\n    get fieldName() {\n        const fName = this.props.node.attrs.name;\n        if (this.canEdit) {\n            return fName.split(\"x_studio_\")[1];\n        }\n        return fName;\n    }\n}\n\nexport class FieldProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Field\";\n    static props = {\n        node: { type: Object },\n        availableOptions: { type: Array, optional: true },\n    };\n    static components = {\n        ClassAttribute,\n        Property,\n        TechnicalName,\n        TypeWidgetProperties,\n        ViewStructureProperties,\n        ModifiersProperties,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.companyService = useService(\"company\");\n        this.multiCompany = Object.keys(this.companyService.allowedCompanies).length > 1;\n        this.state = useState({});\n        this.editNodeAttributes = useEditNodeAttributes();\n        onWillStart(async () => {\n            if (this._canShowDefaultValue(this.props.node)) {\n                this.state.defaultValue = await this.getDefaultValue(this.props.node);\n            }\n        });\n\n        onWillUpdateProps(async (nextProps) => {\n            if (this._canShowDefaultValue(nextProps.node)) {\n                this.state.defaultValue = await this.getDefaultValue(nextProps.node);\n            }\n        });\n    }\n\n    get viewEditorModel() {\n        return this.env.viewEditorModel;\n    }\n\n    async onChangeFieldString(value) {\n        if (this.viewEditorModel.isFieldRenameable(this.props.node.field.name) && value) {\n            return this.viewEditorModel.renameField(this.props.node.attrs.name, value, {\n                label: value,\n            });\n        } else {\n            const operation = {\n                new_attrs: { string: value },\n                type: \"attributes\",\n                position: \"attributes\",\n                target: this.viewEditorModel.getFullTarget(this.viewEditorModel.activeNodeXpath),\n            };\n            // FIXME: the python API is messy: we need to send node, which is the same as target since\n            // we are editing the target's attributes, to be able to modify the python field's string\n            operation.node = operation.target;\n            return this.viewEditorModel.doOperation(operation);\n        }\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n\n    async onChangeDefaultValue(value) {\n        await rpc(\"/web_studio/set_default_value\", {\n            model_name: this.env.viewEditorModel.resModel,\n            field_name: this.props.node.field.name,\n            value,\n            company_id: this.companyService.currentCompany.id,\n        });\n        this.state.defaultValue = value;\n    }\n\n    getBoldValue() {\n        const classList = this.props.node.arch.classList;\n        return (\n            classList.contains(\"fw-bold\") ||\n            classList.contains(\"fw-bolder\") ||\n            this.props.node.attrs.bold // legacy kanban\n        );\n    }\n\n    async getDefaultValue(node) {\n        const defaultValueObj = await rpc(\"/web_studio/get_default_value\", {\n            model_name: this.env.viewEditorModel.resModel,\n            field_name: node.field.name,\n            company_id: this.companyService.currentCompany.id,\n        });\n        return defaultValueObj.default_value;\n    }\n\n    get optionalVisibilityChoices() {\n        return {\n            choices: [\n                { label: _t(\"Show by default\"), value: \"show\" },\n                { label: _t(\"Hide by default\"), value: \"hide\" },\n            ],\n        };\n    }\n\n    getDefaultValuePropertyProps() {\n        if (!this._canShowDefaultValue(this.props.node)) {\n            return null;\n        }\n        const { field, attrs } = this.props.node;\n        const props = {\n            childProps: {},\n            inputAttributes: {},\n        };\n        if (field.selection) {\n            props.childProps.choices = this.props.node.field.selection.map(([value, label]) => {\n                return {\n                    label,\n                    value,\n                };\n            });\n        }\n        const fieldType = field.type;\n        const widget = attrs.widget;\n        props.type = fieldType;\n        if (widget === \"statusbar\") {\n            props.type = \"selection\";\n        }\n        return props;\n    }\n\n    _canShowDefaultValue(node) {\n        if (/^(in_group_|sel_groups_)/.test(node.attrs.name)) {\n            return false;\n        }\n        return ![\"image\", \"many2many\", \"one2many\", \"many2one\", \"binary\"].includes(node.field.type);\n    }\n\n    get canEditSelectionChoices() {\n        return this.props.node.field.manual && this.props.node.field.type === \"selection\";\n    }\n\n    /**\n     * @param {string} name of the attribute\n     * @returns if this attribute supported in the current view\n     */\n    isAttributeSupported(name) {\n        return this.props.availableOptions?.includes(name);\n    }\n\n    editSelectionChoices() {\n        const field = this.props.node.field;\n        this.dialog.add(SelectionContentDialog, {\n            defaultChoices: toRaw(field).selection.map((s) => [...s]),\n            onConfirm: async (choices) => {\n                const result = await rpc(\"/web_studio/edit_field\", {\n                    model_name: this.env.viewEditorModel.resModel,\n                    field_name: field.name,\n                    values: { selection: JSON.stringify(choices) },\n                    force_edit: false,\n                });\n                let reflectChanges = !result;\n                if (result && result.records_linked) {\n                    reflectChanges = false;\n                    await new Promise((resolve) => {\n                        this.dialog.add(ConfirmationDialog, {\n                            body:\n                                result.message ||\n                                _t(\"Are you sure you want to remove the selection values?\"),\n                            confirm: async () => {\n                                await rpc(\"/web_studio/edit_field\", {\n                                    model_name: this.env.viewEditorModel.resModel,\n                                    field_name: field.name,\n                                    values: { selection: JSON.stringify(choices) },\n                                    force_edit: true,\n                                });\n                                reflectChanges = true;\n                                resolve();\n                            },\n                            cancel: () => resolve(),\n                        });\n                    });\n                }\n                if (reflectChanges) {\n                    field.selection = choices;\n                }\n            },\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, onWillRender } from \"@odoo/owl\";\nimport { Record } from \"@web/model/record\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\n\nexport class LimitGroupVisibility extends Component {\n    static template = \"web_studio.ViewEditor.LimitGroupVisibility\";\n    static components = {\n        Record,\n        MultiRecordSelector,\n    };\n    static props = {\n        node: { type: Object },\n    };\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n        onWillRender(() => {\n            const groups = JSON.parse(this.props.node.attrs.studio_groups || \"[]\");\n            this.allowGroups = [];\n            this.forbidGroups = [];\n            this.currentGroups = [];\n            for (const group of groups) {\n                const groupId = group.id;\n                this.currentGroups.push(groupId);\n                if (group.forbid) {\n                    this.forbidGroups.push(groupId);\n                } else {\n                    this.allowGroups.push(groupId);\n                }\n            }\n        })\n    }\n\n    handleNodeGroupsChange(allow, forbid) {\n        allow = new Set(allow || this.allowGroups);\n        forbid = new Set(forbid || this.forbidGroups);\n        if (!allow.isDisjointFrom(forbid)) {\n            throw new Error(\"Cannot allow and forbid at the same time\");\n        }\n        const resIds = [];\n        for (const g of allow) {\n            resIds.push(g);\n        }\n        for (const g of forbid) {\n            resIds.push(`!${g}`)\n        }\n        return this.editNodeAttributes({ groups: resIds });\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n\n    get allowGroupsProps() {\n        return {\n            resModel: \"res.groups\",\n            domain: [[\"id\", \"not in\", this.currentGroups]],\n            resIds: this.allowGroups,\n            update: (resIds) => this.handleNodeGroupsChange(resIds, null),\n        };\n    }\n\n    get forbidGroupsProps() {\n        return {\n            resModel: \"res.groups\",\n            domain: [[\"id\", \"not in\", this.currentGroups]],\n            resIds: this.forbidGroups,\n            update: (resIds) => this.handleNodeGroupsChange(null, resIds),\n        };\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { ExpressionEditorDialog } from \"@web/core/expression_editor_dialog/expression_editor_dialog\";\n\nexport class ModifiersProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Modifiers\";\n    static components = { CheckBox };\n    static props = {\n        node: { type: Object },\n        availableOptions: { type: Array },\n    };\n\n    setup() {\n        this.addDialog = useOwnedDialogs();\n    }\n\n    /**\n     * @param {string} name of the attribute\n     * @returns if this attribute supported in the current view\n     */\n    isAttributeSupported(name) {\n        return this.props.availableOptions?.includes(name);\n    }\n\n    // <tag invisible=\"EXPRESSION\"  />\n    onChangeModifier(name, value) {\n        const isTypeBoolean = typeof value === \"boolean\";\n        const encodesBoolean = isTypeBoolean || this.isBooleanExpression(value);\n        const isTruthy = encodesBoolean ? this.isBoolTrue(value) : !!value;\n        const newAttrs = {};\n        const oldAttrs = { ...this.props.node.attrs };\n\n        const changingInvisible = name === \"invisible\";\n        const isInList = this.env.viewEditorModel.viewType === \"list\";\n\n        if (encodesBoolean) {\n            if (changingInvisible && isInList) {\n                if (isTruthy) {\n                    newAttrs[\"column_invisible\"] = \"True\";\n                } else {\n                    newAttrs[\"column_invisible\"] = \"False\";\n                    newAttrs[\"invisible\"] = \"False\";\n                }\n            } else {\n                newAttrs[name] = isTruthy ? \"True\" : \"False\";\n            }\n        } else {\n            newAttrs[name] = value;\n            if (changingInvisible && isInList && \"column_invisible\" in oldAttrs) {\n                newAttrs[\"column_invisible\"] = \"False\";\n            }\n        }\n\n        if (this.env.viewEditorModel.viewType === \"form\" && name === \"readonly\") {\n            newAttrs.force_save = isTruthy ? \"1\" : \"0\";\n        }\n\n        const operation = {\n            new_attrs: newAttrs,\n            type: \"attributes\",\n            position: \"attributes\",\n            target: this.env.viewEditorModel.getFullTarget(\n                this.env.viewEditorModel.activeNodeXpath\n            ),\n        };\n        this.env.viewEditorModel.doOperation(operation);\n    }\n\n    getCheckboxClassName(value) {\n        if (value && !this.isBooleanExpression(value)) {\n            return \"o_web_studio_checkbox_indeterminate\";\n        }\n    }\n\n    isBooleanExpression(expression) {\n        return [\"1\", \"0\", \"True\", \"true\", \"False\", \"false\"].includes(expression);\n    }\n\n    isBoolTrue(value) {\n        if (typeof value === \"boolean\") {\n            return value;\n        }\n        return [\"1\", \"True\", \"true\"].includes(value);\n    }\n\n    valueAsBoolean(expression) {\n        if (!expression) {\n            return false;\n        }\n        if (this.isBooleanExpression(expression)) {\n            return this.isBoolTrue(expression);\n        }\n        return true;\n    }\n\n    onConditionalButtonClicked(name, value) {\n        if (typeof value !== \"string\" || value === \"\") {\n            value = \"False\"; // See py.js:evaluateBooleanExpr default value is False\n        }\n        const { fields, resModel } = this.env.viewEditorModel;\n        this.addDialog(ExpressionEditorDialog, {\n            resModel,\n            fields,\n            expression: value,\n            onConfirm: (expression) => this.onChangeModifier(name, expression),\n        });\n    }\n}\n", "import { Component, useState, xml } from \"@odoo/owl\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\n\nclass DefaultProperties extends Component {\n    static props = {\n        node: { type: Object },\n    };\n    static template = xml`\n        <SidebarPropertiesToolbox/>\n    `;\n    static components = { SidebarPropertiesToolbox };\n}\n\nexport class Properties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties\";\n    static props = {\n        propertiesComponents: { type: Object },\n    };\n    static components = { DefaultProperties };\n\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n    }\n\n    get iconClass() {\n        // first check if the structure has a dedicated icon\n        let icon =\n            this.env.viewEditorModel.editorInfo.editor.Sidebar.viewStructures?.[this.nodeType]\n                ?.class;\n        if (!icon && this.nodeType === \"field\") {\n            icon = `o_web_studio_field_${this.node.field.type}`;\n        }\n        return icon || `o_web_studio_field_${this.nodeType}`;\n    }\n\n    get node() {\n        return this.viewEditorModel.activeNode;\n    }\n\n    get propertiesComponent() {\n        return this.props.propertiesComponents[this.nodeType] || {};\n    }\n\n    get nodeType() {\n        return this.node?.arch.tagName;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class SidebarPropertiesToolbox extends Component {\n    static props = {};\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Toolbox\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n    }\n\n    get node() {\n        return this.env.viewEditorModel.activeNode;\n    }\n\n    get nodeType() {\n        return this.node.arch.tagName;\n    }\n\n    onRemoveFromView() {\n        this.dialog.add(ConfirmationDialog, {\n            body: _t(\n                \"Are you sure you want to remove this %s from the view?\",\n                this.node.humanName.toLowerCase()\n            ),\n            confirm: () => {\n                return this.removeNodeFromArch();\n            },\n            cancel: () => {},\n        });\n    }\n\n    async openFormAction() {\n        const resId = await this.orm.searchRead(\n            \"ir.model.fields\",\n            [\n                [\"model\", \"=\", this.env.viewEditorModel.resModel],\n                [\"name\", \"=\", this.node.field.name],\n            ],\n            [\"id\"]\n        );\n        return this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"ir.model.fields\",\n                res_id: resId[0].id,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            },\n            { clearBreadcrumbs: true }\n        );\n    }\n\n    removeNodeFromArch(xpath) {\n        const target = this.env.viewEditorModel.getFullTarget(xpath || this.node.xpath);\n        const operation = {\n            type: \"remove\",\n            target,\n        };\n        this.env.viewEditorModel.resetSidebar();\n        return this.env.viewEditorModel.doOperation(operation);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\n/**\n * This object describes the properties editable in studio, depending on\n * one or more attribute of a field. The TypeWidgetProperties component will\n * retrieve the value by itself, or you can set a function with the `getValue`\n * key to compute it specifically for one editable property.\n */\nconst EDITABLE_FIELD_ATTRIBUTES = {\n    context: {\n        name: \"context\",\n        label: _t(\"Context\"),\n        type: \"string\",\n    },\n    domain: {\n        name: \"domain\",\n        label: _t(\"Domain\"),\n        type: \"domain\",\n        getValue({ attrs, field }) {\n            return {\n                domain: attrs.domain,\n                relation: field.relation,\n            };\n        },\n    },\n    aggregate: {\n        name: \"aggregate\",\n        label: _t(\"Aggregate\"),\n        type: \"selection\",\n        choices: [\n            { value: \"sum\", label: _t(\"Sum\") },\n            { value: \"avg\", label: _t(\"Average\") },\n            { value: \"none\", label: _t(\"No aggregation\") },\n        ],\n        getValue({ attrs }) {\n            return attrs.sum ? \"sum\" : attrs.avg ? \"avg\" : \"none\";\n        },\n    },\n    placeholder: {\n        name: \"placeholder\",\n        label: _t(\"Placeholder\"),\n        type: \"string\",\n        help: _t(\"Displays a textual hint that helps the user when the field is empty.\"),\n    },\n};\n\nexport const FIELD_TYPE_ATTRIBUTES = {\n    char: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n    },\n    date: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n    },\n    datetime: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n    },\n    float: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n        list: [EDITABLE_FIELD_ATTRIBUTES.aggregate],\n    },\n    html: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n    },\n    integer: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n        list: [EDITABLE_FIELD_ATTRIBUTES.aggregate],\n    },\n    many2many: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.domain, EDITABLE_FIELD_ATTRIBUTES.context],\n    },\n    many2one: {\n        common: [\n            EDITABLE_FIELD_ATTRIBUTES.domain,\n            EDITABLE_FIELD_ATTRIBUTES.context,\n            EDITABLE_FIELD_ATTRIBUTES.placeholder,\n        ],\n    },\n    monetary: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n        list: [EDITABLE_FIELD_ATTRIBUTES.aggregate],\n    },\n    selection: {\n        common: [EDITABLE_FIELD_ATTRIBUTES.placeholder],\n    },\n};\n\n/**\n * Computed Options are options that are tied to another option.\n * Their value and visibility depends on another option present in the sidebar.\n *\n * They must be documented using 'supportedOptions' on any field widget.\n * Then, register them under COMPUTED_DISPLAY_OPTIONS using the technical name of the option.\n *\n * Here is how to declare them :\n *\n *      COMPUTED_DISPLAY_OPTIONS = {\n *          dependent_option: {\n *              superOption (string): technical name of another option that has an impact on the dependent option.\n *                                      This option must also be documented under 'supportedOptions'.\n *              getValue (function): compute the value of the dependent option from super option value\n *              getReadonly (function): compute a boolean based on the super value.\n *                                      If true, the option is greyed out and it is not possible to interact with them.\n *                                      Otherwise, the dependent option can still be edited.\n *              getInvisible (function): compute a boolean based on the super value.\n *                                      If true, the option is not present in the sidebar.\n *          },\n *          ...\n *      }\n *\n */\n\nexport const COMPUTED_DISPLAY_OPTIONS = {\n    collaborative_trigger: {\n        superOption: \"collaborative\",\n        getInvisible: (value) => !value,\n    },\n    no_quick_create: {\n        superOption: \"no_create\",\n        getValue: (value) => value,\n        getReadonly: (value) => value,\n    },\n    no_create_edit: {\n        superOption: \"no_create\",\n        getValue: (value) => value,\n        getReadonly: (value) => value,\n    },\n    decimals: {\n        superOption: \"human_readable\",\n        getInvisible: (value) => !value,\n    },\n    zoom_delay: {\n        superOption: \"zoom\",\n        getInvisible: (value) => !value,\n    },\n    placeholder_field: {\n        superOption: \"placeholder\",\n    },\n    edit_max_value: {\n        superOption: \"editable\",\n        getInvisible: (value) => !value,\n    },\n    no_edit_color: {\n        superOption: \"color_field\",\n        getInvisible: (value) => !value,\n    },\n};\n", "import { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport {\n    fieldsToChoices,\n    getWowlFieldWidgets,\n} from \"@web_studio/client_action/view_editor/editors/utils\";\nimport {\n    FIELD_TYPE_ATTRIBUTES,\n    COMPUTED_DISPLAY_OPTIONS,\n} from \"@web_studio/client_action/view_editor/interactive_editor/properties/type_widget_properties/type_specific_and_computed_properties\";\n\nexport class TypeWidgetProperties extends Component {\n    static template =\n        \"web_studio.ViewEditor.InteractiveEditorProperties.Field.TypeWidgetProperties\";\n    static components = { Property };\n    static props = {\n        node: { type: Object },\n        onChangeAttribute: { type: Function },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.attributes = useState({\n            field: [],\n            selection: [],\n            boolean: [],\n            domain: [],\n            number: [],\n            string: [],\n            digits: [],\n        });\n\n        onWillStart(async () => {\n            await this.computeAttributesList(this.props);\n        });\n\n        onWillUpdateProps(async (nextProps) => {\n            await this.computeAttributesList(nextProps);\n        });\n    }\n\n    async computeAttributesList(props) {\n        this.attributesForCurrentTypeAndWidget = this.getAttributesForCurrentTypeAndWidget(props);\n        await this.groupAttributesPerType(props);\n    }\n\n    async groupAttributesPerType(props) {\n        this.attributes.field = await this.getAttributesOfTypeField(props);\n        this.attributes.selection = this.getWidgetAttributes(\"selection\", props);\n        this.attributes.boolean = this.getWidgetAttributes(\"boolean\", props);\n        this.attributes.domain = this.getWidgetAttributes(\"domain\", props);\n        this.attributes.number = this.getWidgetAttributes(\"number\", props);\n        this.attributes.string = this.getWidgetAttributes(\"string\", props);\n        this.attributes.digits = this.getWidgetAttributes(\"digits\", props);\n    }\n\n    async getAttributesOfTypeField(props) {\n        const fieldAttributes = this.getWidgetAttributes(\"field\", props);\n        if (fieldAttributes.length) {\n            const fields = Object.entries(this.env.viewEditorModel.fields).map(([key, value]) => {\n                return {\n                    ...value,\n                    name: value.name || key,\n                };\n            });\n            // for each attribute looking for a field, compute the choices to display in the SelectMenu\n            await Promise.all(\n                fieldAttributes.map(async (attribute) => {\n                    const choices = await this.getFieldChoices(attribute, fields);\n                    attribute.choices = choices;\n                    this.getOptionObj(attribute.name).choices = choices;\n                })\n            );\n            return fieldAttributes;\n        }\n        return [];\n    }\n\n    getSupportedOptionsAndAttributes(props) {\n        const widgetName = this.isField\n            ? props.node.attrs?.widget || props.node.field?.type\n            : props.node.attrs.name;\n        const itemsRegistry = registry.category(this.isField ? \"fields\" : \"view_widgets\").content;\n        const widgetDescription =\n            itemsRegistry[this.env.viewEditorModel.viewType + \".\" + widgetName] ||\n            itemsRegistry[widgetName];\n        return [\n            // tag the attributes as the edition changes the attribute value instead of a value from the option attribute.\n            ...(widgetDescription?.[1].supportedAttributes || []).map((e) => ({\n                ...e,\n                isAttribute: true,\n            })),\n            ...(widgetDescription?.[1].supportedOptions || []),\n        ];\n    }\n\n    /**\n     * @returns the list of available widgets for the current node\n     */\n    get widgetChoices() {\n        const widgets = getWowlFieldWidgets(\n            this.props.node.field.type,\n            this.props.node.attrs.widget,\n            [],\n            this.env.debug\n        );\n        return {\n            choices: widgets.map(([value, label]) => {\n                label = label ? label : \"\";\n                return {\n                    label: `${label} (${value})`.trim(),\n                    value,\n                };\n            }),\n        };\n    }\n\n    /**\n     * @returns the list of attributes available depending the type of field,\n     * as well the current widget selected. For a widget node, there is no\n     * concept of type, we simply read what supported options and attributes\n     * are on the node\n     */\n    _getAttributesForCurrentTypeAndWidget(props) {\n        if (!props.node.field) {\n            // node is a widget, and there is no 'type' on such elements\n            return JSON.parse(JSON.stringify(this.getSupportedOptionsAndAttributes(props)));\n        }\n\n        this.isField = true;\n\n        const fieldType = props.node.field.type;\n        const { viewType } = this.env.viewEditorModel;\n\n        const fieldCommonViewsProperties = FIELD_TYPE_ATTRIBUTES[fieldType]?.common || [];\n        const fieldSpecificViewProperties = FIELD_TYPE_ATTRIBUTES[fieldType]?.[viewType] || [];\n\n        return [\n            ...fieldCommonViewsProperties.map((e) => ({ ...e, isAttribute: true })),\n            ...fieldSpecificViewProperties.map((e) => ({ ...e, isAttribute: true })),\n            // create a deep copy of the options description to avoid modifying the original objects\n            ...JSON.parse(JSON.stringify(this.getSupportedOptionsAndAttributes(props))),\n        ];\n    }\n\n    getAttributesForCurrentTypeAndWidget(props) {\n        const _attributes = this._getAttributesForCurrentTypeAndWidget(props);\n        _attributes.forEach((property) => {\n            if (COMPUTED_DISPLAY_OPTIONS[property.name]) {\n                const dependentOption = COMPUTED_DISPLAY_OPTIONS[property.name];\n                const superOption = _attributes.find((o) => o.name === dependentOption.superOption);\n                property.isSubOption = true;\n                if (!superOption.subOptions) {\n                    superOption.subOptions = [];\n                }\n                if (!superOption.subOptions.includes(property.name)) {\n                    // only add the subOption if not already present\n                    superOption.subOptions.push(property.name);\n                }\n            }\n        });\n        return _attributes;\n    }\n\n    getOptionObj(optionName) {\n        return this.attributesForCurrentTypeAndWidget.find((o) => o.name === optionName);\n    }\n\n    /**\n     * @param {string} type of the attribute (eg. \"string\", \"boolean\" )\n     * @returns only the given type of attributes for the current field node\n     */\n    getWidgetAttributes(type, props) {\n        return this.attributesForCurrentTypeAndWidget\n            .filter((attribute) => attribute.type === type)\n            .map((attribute) => {\n                if (attribute.isAttribute) {\n                    return this.getPropertyFromAttributes(attribute, props);\n                }\n                return this.getPropertyFromOptions(attribute, props);\n            })\n            .filter((attribute) => attribute !== undefined);\n    }\n\n    async getFieldChoices(attribute, fields) {\n        let availableFields = fields;\n        // Specific code to filter available fields to display is handled here as supportedOptions\n        // is a generic description and don't allow to describe the full spec of an option\n        if (attribute.name === \"fold_field\") {\n            if (this.env.viewEditorModel.activeNode.field.type === \"selection\") {\n                // fold_field is only relevant with relational status with its own model\n                attribute.isInvisible = true;\n                return [];\n            }\n            const fields = await this.orm.call(\n                this.env.viewEditorModel.activeNode.field.relation,\n                \"fields_get\"\n            );\n            availableFields = Object.values(fields);\n        } else if (attribute.name === \"currency_field\") {\n            availableFields = availableFields.filter((f) => f.relation === \"res.currency\");\n        }\n        return fieldsToChoices(\n            availableFields,\n            attribute.availableTypes,\n            (f) => f.name !== this.env.viewEditorModel.activeNode.attrs.name\n        );\n    }\n\n    /**\n     * Compute the property and its value from one or more attributes on the node\n     */\n    getPropertyFromAttributes(property, props) {\n        let value;\n        value = props.node.attrs[property.name];\n        if (property.getValue) {\n            const attrs = props.node.attrs || {};\n            const field = props.node.field || {};\n            value = property.getValue({ attrs, field });\n        }\n        if (value === undefined && property.default) {\n            value = property.default;\n        }\n        return {\n            ...property,\n            value,\n        };\n    }\n\n    /**\n     * Compute the property and its value from the `options` attribute on the node\n     */\n    getPropertyFromOptions(property, props) {\n        let value;\n        if (COMPUTED_DISPLAY_OPTIONS[property.name]) {\n            // The display of this property must be computed from the value of the corresponding super option\n            const dependentOption = COMPUTED_DISPLAY_OPTIONS[property.name];\n            const superOption = this.getOptionObj(dependentOption.superOption);\n            const superValue = this.getPropertyFromOptions(superOption, props).value;\n            if (dependentOption.getReadonly) {\n                property.isReadonly = dependentOption.getReadonly(superValue);\n            }\n            if (dependentOption.getValue) {\n                property.value = dependentOption.getValue(superValue);\n                if (property.isReadonly) {\n                    // The property value cannot be edited, return the computed value directly\n                    return property;\n                }\n            }\n            if (dependentOption.getInvisible) {\n                property.isInvisible = dependentOption.getInvisible(superValue);\n            }\n        }\n        value = props.node.attrs.options?.[property.name];\n        if (property.type === \"string\") {\n            value = JSON.stringify(value);\n        }\n        if (property.type === \"boolean\" && value !== undefined) {\n            value = !!value;\n        }\n        if (value === undefined && property.default) {\n            value = property.default;\n        }\n        if (property.name === \"currency_field\" && !value) {\n            value = props.node.field.currency_field;\n        }\n        return {\n            ...property,\n            value,\n        };\n    }\n\n    getSelectValue(value) {\n        return typeof value === \"object\" ? JSON.stringify(value) : value;\n    }\n\n    async onChangeCurrency(value) {\n        const proms = [];\n        proms.push(\n            rpc(\"/web_studio/set_currency\", {\n                model_name: this.env.viewEditorModel.resModel,\n                field_name: this.props.node.field.name,\n                value,\n            })\n        );\n        this.env.viewEditorModel.fields[this.props.node.field.name][\"currency_field\"] = value;\n\n        if (this.env.viewEditorModel.fieldsInArch.includes(value)) {\n            // is the new currency in the view ?\n            await Promise.all(proms).then((results) => {\n                if (results[0] === true) {\n                    this.env.viewEditorModel.fields[this.props.node.field.name][\"currency_field\"] =\n                        value;\n                }\n            });\n            // alter the value of the currently selected currency manually to trigger a re-render of the SelectMenu\n            // with the correct value since we don't pass through doOperations from the ViewEditorModel\n            this.attributes.field = this.attributes.field.map((e) => {\n                if (e.name === \"currency_field\") {\n                    e.value = value;\n                }\n                return e;\n            });\n            return;\n        }\n\n        const currencyNode = {\n            tag: \"field\",\n            attrs: { name: value },\n        };\n\n        const operation = {\n            node: currencyNode,\n            target: this.env.viewEditorModel.getFullTarget(\n                this.env.viewEditorModel.activeNodeXpath\n            ),\n            position: \"after\",\n            type: \"add\",\n        };\n\n        proms.push(this.env.viewEditorModel.doOperation(operation));\n        await Promise.all(proms).then((results) => {\n            if (results[0] === true) {\n                this.env.viewEditorModel.fields[this.props.node.field.name][\"currency_field\"] =\n                    value;\n            }\n        });\n    }\n\n    onChangeWidget(value) {\n        return this.props.onChangeAttribute(value, \"widget\");\n    }\n\n    async onChangeProperty(value, name) {\n        if (\n            [\"show_seconds\", \"show_time\"].includes(name) &&\n            !value &&\n            this.props.node.field.type === \"datetime\" &&\n            !this.props.node.attrs.widget\n        ) {\n            this.onChangeWidget(\"datetime\");\n        } else if (name === \"currency_field\" && this.props.node.field.type === \"monetary\") {\n            await this.onChangeCurrency(value);\n            if (!this.props.node.attrs.options?.[name]) {\n                return;\n            }\n            value = \"\"; // the currency_field arch option will be deleted\n        }\n\n        const currentProperty = this.getOptionObj(name);\n        if (currentProperty.isAttribute) {\n            return this.props.onChangeAttribute(value, name);\n        }\n        const options = { ...this.props.node.attrs.options };\n        if (value || currentProperty.type === \"boolean\") {\n            if (currentProperty.type === \"digits\") {\n                // The digits options is composed of two integers.\n                // The first one is unused and the second one is passed to `toFixed`\n                // from `formatFloat`. It should be an integer between 0 and 20.\n                value = Number(value);\n                if (!Number.isInteger(value) || value < 0 || value > 20) {\n                    return;\n                }\n                options[name] = [value * 2, value];\n            } else if ([\"[\", \"{\"].includes(value[0]) || !isNaN(value)) {\n                options[name] = JSON.parse(value);\n            } else if (currentProperty.type === \"number\") {\n                options[name] = Number(value);\n            } else {\n                options[name] = value;\n            }\n        } else {\n            delete options[name];\n        }\n        this.props.onChangeAttribute(JSON.stringify(options), \"options\");\n    }\n}\n", "import { LimitGroupVisibility } from \"@web_studio/client_action/view_editor/interactive_editor/properties/limit_group_visibility/limit_group_visibility\";\nimport { SidebarPropertiesToolbox } from \"@web_studio/client_action/view_editor/interactive_editor/properties/sidebar_properties_toolbox/sidebar_properties_toolbox\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ViewStructureProperties extends Component {\n    static components = { LimitGroupVisibility, SidebarPropertiesToolbox };\n    static template = \"web_studio.ViewStructureProperties\";\n    static props = {\n        slots: { type: Object },\n    };\n    setup() {\n        this.viewEditorModel = useState(this.env.viewEditorModel);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Property } from \"@web_studio/client_action/view_editor/property/property\";\nimport { ClassAttribute } from \"@web_studio/client_action/view_editor/interactive_editor/properties/class_attribute/class_attribute\";\nimport { TypeWidgetProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/type_widget_properties/type_widget_properties\";\nimport { ViewStructureProperties } from \"@web_studio/client_action/view_editor/interactive_editor/properties/view_structure_properties/view_structure_properties\";\nimport { useEditNodeAttributes } from \"@web_studio/client_action/view_editor/view_editor_model\";\n\nexport class WidgetProperties extends Component {\n    static template = \"web_studio.ViewEditor.InteractiveEditorProperties.Widget\";\n    static components = {\n        ClassAttribute,\n        Property,\n        TypeWidgetProperties,\n        ViewStructureProperties,\n    };\n    static props = {\n        node: { type: Object },\n    };\n\n    setup() {\n        this.editNodeAttributes = useEditNodeAttributes();\n    }\n\n    onChangeAttribute(value, name) {\n        return this.editNodeAttributes({ [name]: value });\n    }\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\n\nexport class SidebarViewToolbox extends Component {\n    static template = \"web_studio.ViewEditor.ViewToolbox\";\n    static props = {\n        canEditXml: { type: Boolean, optional: true },\n        onMore: { type: Function, optional: true },\n        openDefaultValues: { type: Function, optional: true },\n        canEditDefaultValues: { type: Boolean, optional: true },\n    };\n}\n", "/** @odoo-module */\n\nexport function viewGroupByOperation(viewType, type, newValue, oldValue = undefined) {\n    const operation_type = newValue ? \"add\" : \"remove\";\n    const operation = {\n        target: {\n            view_type: viewType,\n            field_names: [operation_type === \"add\" ? newValue : oldValue],\n            operation_type,\n            field_type: type,\n        },\n        type: \"graph_pivot_groupbys_fields\",\n    };\n\n    if (oldValue && newValue) {\n        operation.target.operation_type = \"replace\";\n        operation.target.old_field_names = oldValue;\n    }\n\n    return operation;\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class Property extends Component {\n    static template = \"web_studio.Property\";\n    static components = { CheckBox, SelectMenu, DomainSelectorDialog };\n    static defaultProps = {\n        childProps: {},\n        class: \"\",\n    };\n    static props = {\n        name: { type: String },\n        type: { type: String },\n        value: { optional: true },\n        onChange: { type: Function, optional: true },\n        childProps: { type: Object, optional: true },\n        class: { type: String, optional: true },\n        isReadonly: { type: Boolean, optional: true },\n        slots: {\n            type: Object,\n            optional: true,\n        },\n        tooltip: { type: String, optional: true },\n        inputAttributes: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n    }\n\n    get className() {\n        const propsClass = this.props.class ? this.props.class : \"\";\n        return `o_web_studio_property_${this.props.name} ${propsClass}`;\n    }\n\n    onDomainClicked() {\n        this.dialog.add(DomainSelectorDialog, {\n            resModel: this.props.childProps.relation,\n            domain: this.props.value || \"[]\",\n            isDebugMode: !!this.env.debug,\n            onConfirm: (domain) => this.props.onChange(domain, this.props.name),\n        });\n    }\n\n    onViewOptionChange(value) {\n        this.props.onChange(value, this.props.name);\n    }\n}\n", "/** @odoo-module */\n\nimport { WithSearch } from \"@web/search/with_search/with_search\";\nimport { cleanClickedElements } from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { Component, onError, onMounted, toRaw, useRef, xml, useSubEnv, useEffect } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class StudioView extends Component {\n    static components = { WithSearch };\n    static template = xml`\n        <div t-att-style=\"style\" class=\"w-100\" t-ref=\"viewRenderer\">\n            <WithSearch t-props=\"withSearchProps\" t-slot-scope=\"search\">\n                <t t-component=\"viewEditorModel.editorInfo.editor.Controller\" t-props=\"Object.assign(controllerProps, search)\" />\n            </WithSearch>\n        </div>\n    `;\n    static props = { autoClick: { type: Function, optional: true }, \"*\": true }; // Same as View.js. This is just a wrapper\n    setup() {\n        this.notification = useService(\"notification\");\n        this.style = this.props.setOverlay ? \"pointer-events: none;\" : \"\";\n        this.withSearchProps = {\n            resModel: this.props.resModel,\n            SearchModel: this.props.SearchModel,\n            context: this.props.context,\n            domain: this.props.domain,\n            globalState: this.props.globalState,\n            searchViewArch: this.props.searchViewArch,\n            searchViewFields: this.props.searchViewFields,\n            irFilters: this.props.searchViewIrFilters,\n            display: this.props.display,\n        };\n        this.viewEditorModel = this.env.viewEditorModel;\n\n        this.viewRenderer = useRef(\"viewRenderer\");\n\n        this.controllerProps = { ...this.viewEditorModel.controllerProps };\n        if (this.viewEditorModel.initialState.activeNodeXpath) {\n            onMounted(() => {\n                const initialActiveNodeXpath = this.viewEditorModel.initialState.activeNodeXpath;\n                this.viewEditorModel.initialState.activeNodeXpath = null;\n                this.viewEditorModel.activeNodeXpath = initialActiveNodeXpath;\n            });\n        }\n        useEffect(\n            (xpath) => {\n                if (xpath) {\n                    this.updateActiveNode({ xpath, resetSidebarOnNotFound: true });\n                }\n            },\n            () => [this.viewEditorModel.activeNodeXpath]\n        );\n\n        const rawModel = toRaw(this.viewEditorModel);\n        useEffect(\n            () => {\n                rawModel.isInEdition = false;\n            },\n            () => [rawModel.isInEdition]\n        );\n\n        onError((error) => {\n            if (rawModel.isInEdition) {\n                this.notification.add(\n                    _t(\n                        \"The requested change caused an error in the view. It could be because a field was deleted, but still used somewhere else.\"\n                    ),\n                    {\n                        type: \"danger\",\n                        title: _t(\"Error\"),\n                    }\n                );\n                this.viewEditorModel.resetSidebar(\"view\");\n                this.viewEditorModel._operations.undo(false);\n            } else {\n                throw error;\n            }\n        });\n\n        const config = {\n            ...this.env.config,\n            onNodeClicked: (xpath) => {\n                if (this.updateActiveNode({ xpath })) {\n                    this.viewEditorModel.activeNodeXpath = xpath;\n                }\n            },\n        };\n\n        if (this.props.autoClick) {\n            onMounted(() => this.props.autoClick());\n        }\n\n        useSubEnv({\n            config,\n            __beforeLeave__: null,\n            __getGlobalState__: null,\n            __getLocalState__: null,\n            __getContext__: null,\n            __getOrderBy__: null,\n        });\n    }\n\n    updateActiveNode({ xpath, resetSidebarOnNotFound = false }) {\n        const vem = this.env.viewEditorModel;\n        cleanClickedElements(this.viewRenderer.el);\n        const el = this.viewRenderer.el.querySelector(\n            `[data-studio-xpath=\"${xpath}\"], [studioxpath=\"${xpath}\"]`\n        );\n        if (!el) {\n            if (resetSidebarOnNotFound) {\n                vem.resetSidebar();\n            }\n            return false;\n        }\n        if (vem.editorInfo.editor.styleClickedElement) {\n            vem.editorInfo.editor.styleClickedElement(this.viewRenderer, { xpath });\n            return true;\n        }\n        const clickable = el.closest(\".o-web-studio-editor--element-clickable\");\n        if (clickable) {\n            clickable.classList.add(\"o-web-studio-editor--element-clicked\");\n        }\n        return true;\n    }\n}\n", "/** @odoo-module */\nimport { Component, onWillUpdateProps, useState, useSubEnv, useRef, markRaw } from \"@odoo/owl\";\n\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { StudioView } from \"@web_studio/client_action/view_editor/studio_view\";\n\nimport { InteractiveEditor } from \"./interactive_editor/interactive_editor\";\nimport { useViewEditorModel } from \"./view_editor_hook\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { getDefaultConfig } from \"@web/views/view\";\n\nimport { XmlResourceEditor } from \"@web_studio/client_action/xml_resource_editor/xml_resource_editor\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nclass ViewXmlEditor extends XmlResourceEditor {\n    static props = { ...XmlResourceEditor.props, studioViewArch: { type: String } };\n    setup() {\n        super.setup();\n        this.viewEditorModel = this.env.viewEditorModel;\n        useBus(this.viewEditorModel.bus, \"error\", () => this.render(true));\n        this.studioViewState = useState({ arch: this.props.studioViewArch });\n\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.studioViewArch !== this.props.studioViewArch) {\n                const studioResource = this.getStudioResource(this.state.resourcesOptions);\n                if (studioResource) {\n                    studioResource.value.arch = nextProps.studioViewArch;\n                }\n            }\n        });\n    }\n\n    getStudioResource(resourcesOptions) {\n        return resourcesOptions.find((opt) => opt.value.id === this.viewEditorModel.studioViewId);\n    }\n}\n\nexport class ViewEditor extends Component {\n    static props = { ...standardActionServiceProps };\n    static components = { StudioView, InteractiveEditor, ViewXmlEditor };\n    static template = \"web_studio.ViewEditor\";\n\n    static displayName = _t(\"View Editor\");\n\n    setup() {\n        /* Services */\n        this.studio = useService(\"studio\");\n        this.orm = useService(\"orm\");\n        /* MISC */\n        // Avoid pollution from the real actionService's env\n        // Set config compatible with View.js\n        useSubEnv({ config: getDefaultConfig() });\n\n        // Usefull for drag/drop\n        this.rootRef = useRef(\"root\");\n        this.rendererRef = useRef(\"viewRenderer\");\n\n        const initialState = {};\n        const breadcrumbs = this.env.editionFlow.breadcrumbs;\n        if (breadcrumbs.length) {\n            initialState.showInvisible = breadcrumbs[0].initialState.showInvisible;\n            initialState.activeNodeXpath = breadcrumbs.at(-1).initialState.activeNodeXpath;\n        }\n\n        this.viewEditorModel = useViewEditorModel(this.rendererRef, { initialState });\n\n        useSetupAction({\n            getLocalState: () => {\n                // Use this as a hook that is triggered when the actionService knows\n                // this component will be unmounted, is still alive and the new action\n                // is being built.\n                // We store the state in the breadcrumbs, because there two ways\n                // to respawn the editor:\n                // - the editor's breadcrumbs\n                // - the standard actionService breadcrumbs\n                const breadcrumbs = this.viewEditorModel.breadcrumbs;\n                breadcrumbs[0].initialState = markRaw({\n                    showInvisible: this.viewEditorModel.showInvisible,\n                });\n                const last = breadcrumbs.at(-1);\n                last.initialState = markRaw({\n                    ...(last.initialState || {}),\n                    activeNodeXpath: this.viewEditorModel.activeNodeXpath,\n                });\n            },\n        });\n    }\n\n    get interactiveEditorKey() {\n        const { viewType, breadcrumbs } = this.viewEditorModel;\n        let key = viewType;\n        if (breadcrumbs.length > 1) {\n            key += `_${breadcrumbs.length}`;\n        }\n        return key;\n    }\n\n    onSaveXml({ resourceId, oldCode, newCode }) {\n        this.viewEditorModel.doOperation({\n            type: \"replace_arch\",\n            viewId: resourceId,\n            oldArch: oldCode,\n            newArch: newCode,\n        });\n    }\n\n    onXmlEditorClose() {\n        this.viewEditorModel.switchMode();\n    }\n}\nregistry.category(\"actions\").add(\"web_studio.view_editor\", ViewEditor);\n", "/** @odoo-module */\nimport {\n    onWillDestroy,\n    onWillStart,\n    status,\n    useComponent,\n    useEnv,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { viewTypeToString } from \"@web_studio/studio_service\";\nimport {\n    useEditorBreadcrumbs,\n    useEditorMenuItem,\n} from \"@web_studio/client_action/editor/edition_flow\";\nimport { ViewEditorModel } from \"./view_editor_model\";\nimport { ViewEditorSnackbar } from \"./view_editor_snackbar\";\n\nexport function useViewEditorModel(viewRef, { initialState }) {\n    const env = useEnv();\n\n    /* Services */\n    const services = Object.fromEntries(\n        [\"orm\", \"ui\", \"notification\"].map((sName) => {\n            return [sName, useService(sName)];\n        })\n    );\n    // Capture studio's state as a new Object. This is due to concurrency\n    // issues because we are an action, and rendering may be caused by other things (reactives)\n    services.studio = { ...env.services.studio };\n    services.dialog = { add: useOwnedDialogs() };\n\n    /* Coordination */\n    // Communicates with editorMenu, provides standard server calls\n    const editionFlow = useState(env.editionFlow);\n    useEditorBreadcrumbs({ name: viewTypeToString(services.studio.editedViewType) });\n\n    const viewEditorModel = new ViewEditorModel({\n        env,\n        services,\n        editionFlow,\n        viewRef,\n        initialState,\n    });\n    useSubEnv({ viewEditorModel });\n\n    const { _snackBar, _operations } = viewEditorModel;\n    useEditorMenuItem({\n        component: ViewEditorSnackbar,\n        props: { operations: _operations, saveIndicator: _snackBar },\n    });\n\n    const component = useComponent();\n    onWillStart(async () => {\n        return new Promise((resolve, reject) => {\n            viewEditorModel\n                .load()\n                .then(resolve)\n                .catch((error) => {\n                    if (status(component) !== \"destroyed\") {\n                        reject(error);\n                    }\n                });\n        });\n    });\n\n    onWillDestroy(() => {\n        viewEditorModel.isInEdition = false;\n    });\n    return useState(viewEditorModel);\n}\n\nexport function useSnackbarWrapper(fn) {\n    const env = useEnv();\n    return env.viewEditorModel._decorateFunction(fn);\n}\n", "/** @odoo-module */\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport {\n    computeXpath,\n    getNodesFromXpath,\n    getNodeAttributes,\n    parseStringToXml,\n    serializeXmlToString,\n} from \"@web_studio/client_action/view_editor/editors/xml_utils\";\nimport { EventBus, markRaw, useEnv, reactive, toRaw } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { viewTypeToString } from \"@web_studio/studio_service\";\nimport {\n    xpathToLegacyXpathInfo,\n    cleanClickedElements,\n} from \"@web_studio/client_action/view_editor/editors/utils\";\nimport { Reactive, getFieldsInArch, memoizeOnce } from \"@web_studio/client_action/utils\";\nimport { getModifier, resetViewCompilerCache } from \"@web/views/view_compiler\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { EditorOperations, SnackbarIndicator } from \"@web_studio/client_action/editor/edition_flow\";\nimport { Race } from \"@web/core/utils/concurrency\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nconst editorsRegistry = registry.category(\"studio_editors\");\nconst viewRegistry = registry.category(\"views\");\n\nclass EditorOperationsWithSnackbar extends EditorOperations {\n    constructor(params) {\n        super(...arguments);\n        this.snackBar = params.snackBar;\n        this.race = markRaw(new Race());\n    }\n\n    _wrapPromise(prom) {\n        const _prom = super._wrapPromise(prom);\n        this.snackBar.add(this.race.add(_prom));\n        return _prom;\n    }\n}\n\n/**\n * Determines whether a given x2m field has a subview corresponding to archTag.\n * it returns hasArch, true if there is one usable arch already\n * and position, an index, starting at 1, to locate the node via an xpath\n * If there is no arch, hasArch is false, and we expect to go through \"createInlineView\"\n * The position is then the position of the future arch node\n */\nfunction getSubarchPosition(mainArch, xpathToField, archTag) {\n    // get eligible arch nodes, which were not automatically inlined by the server\n    const xpathToArch = `${xpathToField}/${archTag}[not(@studio_subview_inlined)]`;\n    const nodes = getNodesFromXpath(xpathToArch, parseStringToXml(mainArch));\n    let hasArch = false;\n    let position = 1;\n    for (const node of nodes) {\n        // When a subarch has groups=\"somegroup\" and the user doesn't have those groups\n        // The server makes it invisible via the modifiers.\n        if (getModifier(node, \"invisible\") !== \"True\" && getModifier(node, \"invisible\") !== \"1\") {\n            hasArch = true;\n            break;\n        }\n        position++;\n    }\n    return { hasArch, position };\n}\n\n/**\n * Returns the arch of the subview\n *\n * @param {String} mainArch\n * @param {String} xpathToField\n * @param {String} viewType\n * @param {Number} position\n */\nfunction getSubArch(mainArch, xpathToField, archTag, position) {\n    const xpathToView = `${archTag}[${position}]`;\n    const xpathToArch = `${xpathToField}/${xpathToView}`;\n    const nodes = getNodesFromXpath(xpathToArch, parseStringToXml(mainArch));\n    if (nodes.length !== 1) {\n        throw new Error(`Single sub-view arch not found for xpath: ${xpathToArch}`);\n    }\n    return serializeXmlToString(nodes[0]);\n}\n\nfunction buildKey(...args) {\n    return args.join(\"_\");\n}\n\nexport class ViewEditorModel extends Reactive {\n    constructor({ env, services, editionFlow, viewRef, initialState = {} }) {\n        super();\n        this.initialState = initialState;\n        this._isInEdition = false;\n        this.mode = \"interactive\";\n        this.env = env;\n        this.bus = markRaw(new EventBus());\n        this._services = markRaw(services);\n        this._studio = services.studio;\n\n        this._snackBar = new SnackbarIndicator();\n        this._operations = new EditorOperationsWithSnackbar({\n            do: this._handleOperations.bind(this),\n            onDone: this._handleDone.bind(this),\n            onError: this._handleError.bind(this),\n            snackBar: this._snackBar,\n        });\n\n        this._decorateCall = async (callback, ...args) => {\n            this._services.ui.block();\n            const prom = callback(...args);\n            this._snackBar.add(prom);\n            try {\n                return await prom;\n            } finally {\n                this._services.ui.unblock();\n            }\n        };\n        this._decorateFunction = (callback) => {\n            return async (...args) => {\n                return this._decorateCall(callback, ...args);\n            };\n        };\n\n        this._decoratedRpc = this._decorateFunction(rpc);\n\n        this._editionFlow = editionFlow;\n\n        this.GROUPABLE_TYPES = [\"many2one\", \"char\", \"boolean\", \"selection\", \"date\", \"datetime\"];\n\n        this._activeNodeXpath = undefined;\n        this.lastActiveNodeXpath = undefined;\n\n        this._getEditor = memoizeOnce(() => {\n            let viewType = this.viewType;\n            const view = viewRegistry.contains(viewType) ? viewRegistry.get(viewType) : null;\n            //FIXME remove as soon as the legacy api is removed (post v18)\n            if (viewType === \"kanban\" && !this.mainArch.includes('t-name=\"card\"')) {\n                viewType = \"kanban_legacy\";\n            }\n            const editor = editorsRegistry.contains(viewType)\n                ? editorsRegistry.get(viewType)\n                : null;\n\n            // When the mode is interactive, the priority is to get the taylor-made editor if it exists.\n            // otherwise, the priority is to get the view, if it exists (e.g.:, the search editor doesn't have a view)\n            return {\n                getProps: editor ? editor.props : view.props,\n                editor: this.mode === \"interactive\" ? editor || view : view || editor,\n            };\n        });\n\n        this._getControllerProps = memoizeOnce(function () {\n            let { resId, resIds } = this.isEditingSubview\n                ? this._subviewInfo\n                : this._studio.editedControllerState || {};\n            resIds = resIds || [];\n            resId = resId || resIds[0];\n\n            const arch = parseXML(this.arch);\n            if (this.mode !== \"interactive\") {\n                arch.querySelectorAll(`[studio_no_fetch=\"1\"]`).forEach((n) => n.remove());\n            }\n\n            const rootArchNode = this.xmlDoc.firstElementChild;\n            const controllerClasses = Array.from(\n                new Set([\n                    \"o_view_controller\",\n                    `o_${this.viewType}_view`,\n                    ...(rootArchNode.getAttribute(\"class\") || \"\").split(\" \"),\n                ])\n            ).filter((c) => c);\n\n            let controllerProps = {\n                info: {},\n                relatedModels: { ...toRaw(this.viewDescriptions.relatedModels) },\n                useSampleModel: [\"graph\", \"pivot\"].includes(this.viewType),\n                searchMenuTypes: [],\n                className: controllerClasses.join(\" \"),\n                resId,\n                resIds,\n                resModel: this.resModel,\n                arch,\n                fields: { ...toRaw(this.fields) },\n            };\n\n            if (\n                [\"list\", \"list\", \"form\"].includes(this.viewType) &&\n                this.mode === \"interactive\" &&\n                this._subviewInfo\n            ) {\n                controllerProps.parentRecord = this._subviewInfo.parentRecord;\n            }\n            // if (custom_view_id) {\n            //     // for dashboard\n            //     controllerProps.info.customViewId = custom_view_id;\n            // }\n\n            const { editor, getProps } = this.editorInfo;\n            controllerProps = getProps\n                ? getProps(controllerProps, editor, this.env.config)\n                : controllerProps;\n\n            return markRaw(controllerProps);\n        });\n\n        this.__getDefaultStudioViewProps = memoizeOnce(() => {\n            const editedAction = this._studio.editedAction;\n            let globalState;\n            if (this._views.search && !this.isEditingSubview) {\n                globalState = editedAction.globalState;\n            }\n\n            const context = this._subviewInfo ? this._subviewInfo.context : editedAction.context;\n            const searchModel = this.editorInfo.editor.SearchModel || SearchModel;\n            return {\n                context: { ...context, studio: 1 },\n                domain: editedAction.domain,\n                resModel: this.resModel,\n                SearchModel: searchModel,\n                setOverlay:\n                    ![\"form\", \"list\", \"list\", \"kanban\", \"search\"].includes(this.viewType) ||\n                    this.mode !== \"interactive\",\n                display: { controlPanel: false, searchPanel: false },\n                globalState,\n            };\n        });\n\n        this._getActiveNode = memoizeOnce(() => {\n            if (!this.activeNodeXpath) {\n                return undefined;\n            }\n\n            const node = getNodesFromXpath(this.activeNodeXpath, this.xmlDoc)[0];\n            if (!node) {\n                return null;\n            }\n            const isField = node.tagName === \"field\";\n            const attrs = getNodeAttributes(node);\n            const humanName =\n                this.editorInfo.editor.Sidebar.viewStructures?.[node.tagName]?.name || node.tagName;\n\n            let field;\n            if (isField) {\n                field = reactive(this.fields[attrs.name]);\n                Object.defineProperty(field, \"label\", {\n                    get() {\n                        return field.string;\n                    },\n                    configurable: true,\n                });\n            }\n            return reactive({\n                arch: node,\n                attrs,\n                humanName,\n                xpath: this.activeNodeXpath,\n                field,\n            });\n        });\n\n        this._getUnprocessedXmlDoc = memoizeOnce((arch) => parseStringToXml(arch));\n\n        this.breadcrumbs = editionFlow.breadcrumbs;\n\n        this._editionFlow = editionFlow;\n\n        this._views = {};\n\n        this.studioViewArch = \"\";\n        this.viewDescriptions = {\n            relatedModels: {},\n            fields: [],\n        };\n        this.viewRef = viewRef;\n\n        this.showInvisible = initialState.showInvisible || false;\n\n        // Keep track of the current sidebarTab to be able to\n        // restore it when switching back from the xml editor\n        // to the interactive editor.\n        this._currentSidebarTab = undefined;\n\n        this._getFieldsAllowedRename = memoizeOnce(() => {\n            return new Set();\n        });\n    }\n\n    //-----------------------------------------------------------------\n    // Public getters and setters\n    //-----------------------------------------------------------------\n    get editorInfo() {\n        return this._getEditor(buildKey(this.viewType, this.mode));\n    }\n\n    get controllerProps() {\n        const key = buildKey(\n            this.arch,\n            this.viewType,\n            this.mode,\n            this.resModel,\n            this.breadcrumbs.length > 1 ? this.breadcrumbs.length : 1\n        );\n        return this._getControllerProps(key);\n    }\n\n    get studioViewProps() {\n        const key = buildKey(this.viewType, this.resModel, this.mode, this.isEditingSubview);\n        return this.__getDefaultStudioViewProps(key);\n    }\n\n    get xmlDoc() {\n        return this._getUnprocessedXmlDoc(this.arch);\n    }\n\n    get isEditingSubview() {\n        return this.breadcrumbs.length > 1;\n    }\n\n    set isInEdition(value) {\n        value = !!value; // enforce boolean\n        if (this.isInEdition === value) {\n            return;\n        }\n        this._isInEdition = value;\n        if (value) {\n            this._services.ui.block();\n        } else {\n            this._services.ui.unblock();\n        }\n    }\n\n    get isInEdition() {\n        return this._isInEdition;\n    }\n\n    get mainView() {\n        return this._views ? this._views[this.mainViewType] : undefined;\n    }\n\n    get mainArch() {\n        return this.mainView ? this.mainView.arch : \"\";\n    }\n\n    get mainViewType() {\n        return this._studio.editedViewType;\n    }\n\n    get mainResModel() {\n        return this._studio.editedAction.res_model;\n    }\n\n    get arch() {\n        return this.isEditingSubview ? this._subviewInfo.getArch(this.mainArch) : this.mainArch;\n    }\n\n    get viewType() {\n        return this.isEditingSubview ? this._subviewInfo.viewType : this.mainViewType;\n    }\n\n    get view() {\n        return this._views[this.viewType];\n    }\n\n    get resModel() {\n        return this.isEditingSubview ? this._subviewInfo.resModel : this.mainResModel;\n    }\n\n    get fields() {\n        return this.viewDescriptions.relatedModels[this.resModel].fields;\n    }\n\n    get activeNode() {\n        return this._getActiveNode(buildKey(this.activeNodeXpath, this.arch));\n    }\n\n    get studioViewKey() {\n        return buildKey(this.arch, JSON.stringify(this.fields));\n    }\n\n    get fieldsInArch() {\n        return getFieldsInArch(this.xmlDoc);\n    }\n\n    get isChatterAllowed() {\n        return !this.isEditingSubview && this._isChatterAllowed;\n    }\n\n    get activeNodeXpath() {\n        return this._activeNodeXpath;\n    }\n\n    set activeNodeXpath(value) {\n        this._activeNodeXpath = value;\n        if (value) {\n            this.lastActiveNodeXpath = value;\n        }\n    }\n\n    get sidebarTab() {\n        if (this.activeNodeXpath) {\n            return \"properties\";\n        }\n        return this._currentSidebarTab;\n    }\n\n    set sidebarTab(newTab) {\n        this._currentSidebarTab = newTab;\n    }\n\n    //-----------------------------------------------------------------\n    // Public methods\n    //-----------------------------------------------------------------\n\n    async editX2ManyView({ viewType, fieldName, record, xpath, fieldContext }) {\n        const staticList = record.data[fieldName];\n        const resIds = staticList.records.map((r) => r.resId).filter(id => !!id);\n        const resModel = staticList.resModel;\n        const archTag = viewType;\n\n        // currentFullXpath is the absolute xpath to the current edited subview as a function of the whole full arch\n        // while xpath is the absolute xpath to the field we want to edit a subarch for, as a function of its subArch\n        // currentFullXpath: /form[x]/field[y]/form[z]\n        // xpath: /form[g]/field[h]/form[i]\n        // Where form[z] and form[g] do point to the same subArch\n        // We need to combine them to get a xpath of the field's arch we want to edit as a function of the entire main arch\n        // what we want: /form[x]/field[y]/form[z]/field[h]/form[i]\n        const currentFullXpath = this.getSubviewXpath();\n        let xpathToField = xpath;\n        if (currentFullXpath) {\n            const xpathWithoutView = xpath.split(\"/\").slice(2);\n            xpathToField = `${currentFullXpath}/${xpathWithoutView.join(\"/\")}`;\n        }\n\n        const { hasArch, position } = getSubarchPosition(this.mainArch, xpathToField, archTag);\n        if (!hasArch) {\n            const subViewRef = fieldContext[`${archTag}_view_ref`] || null;\n            this.studioViewArch = await this._createInlineView({\n                subViewType: viewType,\n                fullXpath: xpathToField,\n                subViewRef,\n                resModel,\n                fieldName,\n            });\n            const viewDescriptions = await this._editionFlow.loadViews();\n            this.viewDescriptions = viewDescriptions;\n            Object.assign(this._views, viewDescriptions.views);\n            this._operations.clear(false);\n        }\n        await this._decorateCall(() => this.fieldsGet(resModel));\n\n        const context = Object.fromEntries(\n            Object.entries(fieldContext).filter(([key, val]) => {\n                return !key.startsWith(\"default_\") && !key.endsWith(\"_view_ref\");\n            })\n        );\n\n        const x2ManyEditionInfo = {\n            name: sprintf(\"Subview %s\", viewTypeToString(viewType)),\n            context,\n            resModel,\n            resId: resIds[0],\n            resIds,\n            viewType,\n            parentRecord: record,\n            xpath: `${xpath}/${archTag}[${position}]`, // /form[x]/field[y]/list[z]\n            fieldName,\n            getArch: memoizeOnce((mainArch) => {\n                return getSubArch(mainArch, xpathToField, archTag, position);\n            }),\n        };\n        this._editionFlow.pushBreadcrumb(x2ManyEditionInfo);\n    }\n\n    async fieldsGet(resModel) {\n        this.fieldsGetCache = this.fieldsGetCache || new Set();\n        if (!this.fieldsGetCache.has(resModel)) {\n            const fg = await this._services.orm.call(resModel, \"fields_get\");\n            this.fieldsGetCache.add(resModel);\n            Object.assign(this.viewDescriptions.relatedModels[resModel].fields, fg);\n        }\n    }\n\n    async load() {\n        const proms = [this._editionFlow.loadViews({ forceSearch: true })];\n\n        if (this.viewType === \"form\") {\n            proms.push(this._studio.isAllowed(\"chatter\", this.mainResModel));\n        }\n\n        const [viewDescriptions, isChatterAllowed] = await Promise.all(proms);\n        this._isChatterAllowed = isChatterAllowed;\n        this.viewDescriptions = viewDescriptions || {\n            relatedModels: {},\n            fields: [],\n        };\n        Object.assign(this._views, viewDescriptions.views);\n        const { mainViewId, viewId, arch } = await this._getStudioViewArch();\n        this.studioViewArch = arch;\n        this.studioViewId = viewId;\n        if (!this.mainView.id) {\n            // the call to getStudioViewArch has created the view in DB (before that, it was the default_view)\n            // Clear the caches, in particular the one of the viewService to aknowledge that.\n            this.env.bus.trigger(\"CLEAR-CACHES\");\n            this.mainView.id = mainViewId;\n        }\n    }\n\n    getSubviewXpath() {\n        if (!this.isEditingSubview) {\n            return null;\n        }\n        const temp = [`/${this.mainViewType}[1]`];\n        this.breadcrumbs.slice(1).forEach(({ data }) => {\n            const withoutView = data.xpath.split(\"/\").slice(2);\n            temp.push(...withoutView);\n        });\n        return temp.join(\"/\");\n    }\n\n    getFullTarget(xpath, { isXpathFullAbsolute = true } = {}) {\n        const nodes = getNodesFromXpath(xpath, this.xmlDoc);\n        if (nodes.length !== 1) {\n            throw new Error(\"Xpath resolved to nothing or multiple nodes\");\n        }\n        const element = nodes[0];\n\n        // Attributes that could be used to identify the node python side, it is mandatory\n        // Although it might be more robust to rely solely on a sufficiently expressive xpath\n        const attrs = {};\n        [\"name\", \"id\", \"class\", \"for\"].forEach((attrName) => {\n            if (element.hasAttribute(attrName)) {\n                attrs[attrName] = element.getAttribute(attrName);\n            }\n        });\n\n        let xpath_info;\n        if (isXpathFullAbsolute) {\n            xpath_info = xpathToLegacyXpathInfo(xpath);\n        } else {\n            const fullAbsolute = computeXpath(element, this.viewType);\n            xpath_info = xpathToLegacyXpathInfo(fullAbsolute);\n        }\n\n        const target = {\n            tag: element.tagName,\n            attrs,\n            xpath_info,\n        };\n\n        const subViewXpath = this.getSubviewXpath();\n        if (subViewXpath) {\n            target.subview_xpath = subViewXpath;\n\n            const subViewTargetInfo = xpathToLegacyXpathInfo(subViewXpath);\n            xpath_info.splice(0, 1, subViewTargetInfo[subViewTargetInfo.length - 1]);\n        }\n        return target;\n    }\n\n    async doOperation(operation, write = true) {\n        return this._operations.do(operation, !write);\n    }\n\n    pushOperation(operation) {\n        return this._operations.pushOp(operation);\n    }\n\n    /** Mode and Sidebar */\n    resetSidebar(tab = null) {\n        this.sidebarTab = tab;\n        // store the last active xpath in this variable\n        this.activeNodeXpath = undefined;\n\n        const resetEl = this.viewRef.el;\n        if (resetEl) {\n            cleanClickedElements(resetEl);\n        }\n    }\n\n    switchMode() {\n        resetViewCompilerCache();\n        this.mode = this.mode === \"interactive\" ? \"xml\" : \"interactive\";\n    }\n\n    /** Field Renaming */\n    setRenameableField(fieldName, add = true) {\n        if (add) {\n            this._fieldsAllowedRename.add(fieldName);\n        } else {\n            this._fieldsAllowedRename.delete(fieldName);\n        }\n    }\n\n    isFieldRenameable(fieldName) {\n        return this._fieldsAllowedRename.has(fieldName);\n    }\n\n    async renameField(fieldName, newName, { label, autoUnique = true } = {}) {\n        // Sanitization\n        newName = newName\n            .toLowerCase()\n            .trim()\n            .replace(/[^\\w\\s-]/g, \"\") // remove non-word [a-z0-9_], non-whitespace, non-hyphen characters\n            .replace(/[\\s_-]+/g, \"_\") // swap any length of whitespace, underscore, hyphen characters with a single _\n            .replace(/^-+|-+$/g, \"\"); // remove leading, trailing\n\n        if (!newName.startsWith(\"x_studio_\")) {\n            newName = `x_studio_${newName}`;\n        }\n\n        const existingFields = this.fields;\n        if (autoUnique) {\n            const baseName = newName;\n            let index = 1;\n            while (newName in existingFields) {\n                newName = baseName + \"_\" + index;\n                index++;\n            }\n        }\n\n        if (!autoUnique && newName in existingFields) {\n            this._services.dialog.add(AlertDialog, {\n                body: _t(\"A field with the same name already exists.\"),\n            });\n            return;\n        }\n        this.isInEdition = true;\n        const prom = rpc(\"/web_studio/rename_field\", {\n            studio_view_id: this.studioViewId,\n            studio_view_arch: this.studioViewArch,\n            model: this.resModel,\n            old_name: fieldName,\n            new_name: newName,\n            new_label: label,\n        });\n\n        this._snackBar.add(prom);\n\n        try {\n            await prom;\n        } catch (e) {\n            this.isInEdition = false;\n            throw e;\n        }\n\n        const strOperations = JSON.stringify(this._operations.operations);\n        // We only want to replace exact matches of the field name, but it can\n        // be preceeded/followed by other characters, like parent.my_field or in\n        // a domain like [('...', '...', my_field)] etc.\n        // Note that negative lookbehind is not correctly handled in JS ...\n        const chars = \"[^\\\\w\\\\u007F-\\\\uFFFF]\";\n        const re = new RegExp(`(${chars}|^)${fieldName}(${chars}|$)`, \"g\");\n        this._operations.clear();\n        this.setRenameableField(fieldName, false);\n        this.setRenameableField(newName, true);\n        this._operations.doMulti(JSON.parse(strOperations.replace(re, `$1${newName}$2`)));\n    }\n\n    //-----------------------------------------------------------------\n    // Private\n    //-----------------------------------------------------------------\n\n    async _createInlineView({ subViewType, fullXpath, subViewRef, resModel, fieldName }) {\n        // We build the correct xpath if we are editing a 'sub' subview\n        // Use specific view if available in context\n        // We write views in the base language to make sure we do it on the source term field\n        // of ir.ui.view\n        const context = { ...user.context, lang: false, studio: true };\n        if (subViewRef) {\n            context[`${subViewType}_view_ref`] = subViewRef;\n        }\n\n        // FIXME: maybe this route should return def _return_view\n        const studioViewArch = await this._decoratedRpc(\"/web_studio/create_inline_view\", {\n            model: resModel,\n            view_id: this.mainView.id,\n            field_name: fieldName,\n            subview_type: subViewType,\n            subview_xpath: fullXpath,\n            context,\n        });\n        this.env.bus.trigger(\"CLEAR-CACHES\");\n        return studioViewArch;\n    }\n\n    /** Arch Edition */\n    async _editView(operations) {\n        const context = {\n            ...user.context,\n            ...(this._studio.editedAction.context || {}),\n            lang: false,\n            studio: true,\n        };\n        return rpc(\"/web_studio/edit_view\", {\n            view_id: this.mainView.id,\n            studio_view_arch: this.studioViewArch,\n            operations: operations,\n            model: this.resModel,\n            context,\n        });\n    }\n\n    async _editViewArch(viewId, viewArch) {\n        const context = {\n            ...user.context,\n            ...(this._studio.editedAction.context || {}),\n            lang: false,\n            studio: true,\n        };\n        const result = await rpc(\"/web_studio/edit_view_arch\", {\n            view_id: viewId,\n            view_arch: viewArch,\n            // We write views in the base language to make sure we do it on the source term field\n            // of ir.ui.view\n            context,\n        });\n        return result;\n    }\n\n    async _handleOperations({ mode, operations, lastOp }) {\n        this.isInEdition = true;\n        if (lastOp.type !== \"replace_arch\") {\n            operations = operations.filter((op) => op.type !== \"replace_arch\");\n            return this._editView(operations);\n        } else {\n            const viewId = lastOp.viewId;\n            let { newArch, oldArch } = lastOp;\n            if (mode === \"undo\") {\n                const _newArch = newArch;\n                newArch = oldArch;\n                oldArch = _newArch;\n            }\n            return this._editViewArch(viewId, newArch);\n        }\n    }\n\n    async restoreDefaultView(viewId) {\n        const result = await this._editionFlow.restoreDefaultView(viewId, this.mainViewType);\n        if (result) {\n            this.viewDescriptions.relatedModels = result.models;\n            this._views[this.mainViewType].arch = result.views[this.mainViewType].arch;\n            this._operations.clear();\n        }\n    }\n\n    _handleDone({ mode, pending, pendingUndone, result }) {\n        this.env.bus.trigger(\"CLEAR-CACHES\");\n        if (this.mainViewType === \"kanban\") {\n            // the cache is on a by-template basis\n            // kanban may have multiple t-name templates\n            // Wipe everything to force re-compilation\n            resetViewCompilerCache();\n        }\n        if (result) {\n            this.viewDescriptions.relatedModels = result.models;\n\n            const oldArch = this._views[this.mainViewType].arch;\n            const newArch = result.views[this.mainViewType].arch;\n            this._views[this.mainViewType].arch = newArch;\n            if (oldArch === newArch) {\n                this.isInEdition = false;\n            }\n\n            if (!this.studioViewId && result.studio_view_id) {\n                this.studioViewId = result.studio_view_id;\n            }\n        }\n\n        const isUndoing = mode === \"undo\";\n        const pendingOps = isUndoing ? pendingUndone : pending;\n        const lastOperation = pendingOps[pendingOps.length - 1];\n        if (lastOperation && lastOperation.type === \"replace_arch\") {\n            if (lastOperation.viewId === this.studioViewId) {\n                this.studioViewArch = isUndoing ? lastOperation.oldArch : lastOperation.newArch;\n                this._operations.clear();\n                const ops = isUndoing ? this._operations.undone : this._operations.operations;\n                ops.push(lastOperation);\n            }\n        }\n    }\n\n    async _handleError({ mode, pending, error }) {\n        this.isInEdition = false;\n        this._services.notification.add(\n            _t(\"This operation caused an error, probably because a xpath was broken\"),\n            {\n                type: \"danger\",\n                title: _t(\"Error\"),\n            }\n        );\n\n        Promise.resolve().then(() => {\n            throw error;\n        });\n\n        this.resetSidebar(\"view\");\n        this.bus.trigger(\"error\");\n    }\n\n    async _getStudioViewArch() {\n        const result = await rpc(\"/web_studio/get_studio_view_arch\", {\n            model: this.resModel,\n            view_type: this.viewType,\n            view_id: this.mainView.id,\n            context: { ...user.context, lang: false },\n        });\n        return {\n            arch: result.studio_view_arch,\n            viewId: result.studio_view_id,\n            mainViewId: result.main_view_id,\n        };\n    }\n\n    get _subviewInfo() {\n        if (!this.isEditingSubview) {\n            return null;\n        }\n        const length = this.breadcrumbs.length;\n        return this.breadcrumbs[length - 1].data;\n    }\n\n    get _fieldsAllowedRename() {\n        return this._getFieldsAllowedRename(\n            this.breadcrumbs.length > 1 ? this.breadcrumbs.length : 1\n        );\n    }\n}\n\nexport function useEditNodeAttributes({ isRoot = false } = {}) {\n    const vem = useEnv().viewEditorModel;\n    function editNodeAttributes(newAttributes) {\n        let target;\n        let node;\n        if (isRoot) {\n            target = vem.getFullTarget(`/${vem.viewType}`);\n            target.isSubviewAttr = true;\n        } else {\n            target = vem.getFullTarget(vem.activeNodeXpath);\n            const { arch, attrs } = vem.activeNode;\n            node = {\n                tag: arch.tagName,\n                attrs,\n            };\n        }\n\n        const operation = {\n            new_attrs: newAttributes,\n            type: \"attributes\",\n            position: \"attributes\",\n            target,\n        };\n        if (node) {\n            operation.node = node;\n        }\n        return vem.doOperation(operation);\n    }\n    return editNodeAttributes;\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\n\nexport class ViewEditorSnackbar extends Component {\n    static template = \"web_studio.ViewEditor.Snackbar\";\n    static props = {\n        operations: Object,\n        saveIndicator: Object,\n    };\n}\n", "/** @odoo-module */\nimport { Component, onWillStart, onWillUpdateProps, toRaw, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ResizablePanel } from \"@web/core/resizable_panel/resizable_panel\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\n\nclass ViewSelector extends SelectMenu {\n    static template = \"web_studio.ViewSelector\";\n    static choiceItemTemplate = \"web_studio.ViewSelector.ChoiceItemRecursive\";\n    static props = {\n        ...SelectMenu.props,\n        choices: {\n            optional: true,\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    value: true,\n                    label: { type: String },\n                    resource: { optional: true },\n                },\n            },\n        },\n    };\n\n    getMainViews() {\n        return this.state.displayedOptions.filter((opt) => opt.resource.isMainResource);\n    }\n\n    getInherited(choice) {\n        const inheritedChoices = this.state.displayedOptions.filter(\n            (opt) => (opt.resource.inherit_id || [])[0] === choice.resource.id\n        );\n        if (inheritedChoices.length) {\n            inheritedChoices.forEach((opt) => (opt.resource.relatedChoice = choice.resource.id));\n        }\n        return inheritedChoices;\n    }\n\n    getComposedBy(choice) {\n        const resource = choice.resource;\n        if (!resource.called_xml_ids) {\n            return [];\n        }\n        const composedChoices = this.state.displayedOptions.filter(\n            (opt) =>\n                resource.called_xml_ids.includes(opt.resource.xml_id) ||\n                resource.called_xml_ids.includes(opt.resource.key)\n        );\n        if (composedChoices.length) {\n            composedChoices.forEach((opt) => (opt.resource.relatedChoice = resource.id));\n        }\n        return composedChoices;\n    }\n\n    // Parents of displayed options must also be visible when doing a search\n    // Based on the filtered choices, they must be added from the list of choices\n    sliceDisplayedOptions() {\n        const childChoices = this.state.choices.filter((c) => c.resource.relatedChoice);\n        childChoices.forEach((c) => this.addRelatedChoice(c.resource.relatedChoice));\n        super.sliceDisplayedOptions();\n    }\n\n    addRelatedChoice(parentId) {\n        if (this.state.choices.findIndex((c) => c.resource.id === parentId) === -1) {\n            const parent = this.props.choices.find((c) => c.resource.id === parentId);\n            if (!parent.resource.isMainResource) {\n                this.addRelatedChoice(parent.resource.relatedChoice);\n            }\n            this.state.choices.push(parent);\n        }\n    }\n}\n\nexport class XmlResourceEditor extends Component {\n    static template = \"web_studio.XmlResourceEditor\";\n    static components = { ResizablePanel, CodeEditor, SelectMenu: ViewSelector };\n    static props = {\n        onClose: { type: Function },\n        onCodeChange: { type: Function, optional: true },\n        onSave: { type: Function, optional: true },\n        mainResourceId: { type: true },\n        defaultResourceId: { type: true, optional: true },\n        getDefaultResource: { optional: true, type: Function },\n        canSave: { type: Boolean, optional: true },\n        minWidth: { type: Number, optional: true },\n        reloadSources: { type: Number, optional: true },\n        displayAlerts: { type: Boolean, optional: true },\n        onResourceChange: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        canSave: true,\n        minWidth: 400,\n        reloadSources: 1,\n        displayAlerts: true,\n        onResourceChange: () => {},\n        getDefaultResource: () => {},\n    };\n\n    setup() {\n        this.state = useState({\n            resourcesOptions: [],\n            currentResourceId: null,\n            _codeChanges: null,\n        });\n        this.codeEditorKey = this.props.reloadSources;\n        onWillStart(() => this.loadResources(this.props.mainResourceId));\n\n        onWillUpdateProps(async (nextProps) => {\n            const shouldReload =\n                nextProps.mainResourceId !== this.props.mainResourceId ||\n                this.codeEditorKey !== nextProps.reloadSources;\n            const nextResourceId =\n                nextProps.mainResourceId !== this.props.mainResourceId\n                    ? nextProps.mainResourceId\n                    : this.state.currentResourceId;\n\n            if (shouldReload) {\n                this.state._codeChanges = null;\n                await this.loadResources(nextProps.mainResourceId);\n                this.state.currentResourceId = nextResourceId;\n            }\n            this.codeEditorKey = nextProps.reloadSources;\n        });\n\n        this.alerts = useState({\n            \"built-in-file\": {\n                message: _t(\n                    \"Editing a built-in file through this editor is not advised, as it will prevent it from being updated during future App upgrades.\"\n                ),\n                display: true,\n            },\n        });\n    }\n\n    get minWidth() {\n        return this.props.minWidth;\n    }\n\n    get arch() {\n        const currentResourceId = this.state.currentResourceId;\n        if (!currentResourceId) {\n            return \"\";\n        }\n        return this.tempCode || this.getResourceFromId(currentResourceId).arch;\n    }\n\n    get tempCode() {\n        if (!this.state.currentResourceId) {\n            return \"\";\n        }\n        return this.state._codeChanges && this.state._codeChanges[this.state.currentResourceId];\n    }\n\n    set tempCode(value) {\n        if (!this.state.currentResourceId) {\n            return;\n        }\n        this.state._codeChanges = this.state._codeChanges || {};\n        this.state._codeChanges[this.state.currentResourceId] = value;\n    }\n\n    getResourceFromId(resourceId) {\n        const opt = this.state.resourcesOptions.find((opt) => opt.value === resourceId) || {};\n        return opt.resource;\n    }\n\n    onFormat() {\n        this.tempCode = window.vkbeautify.xml(this.tempCode || this.arch, 4);\n    }\n\n    hideAlert(alertKey) {\n        this.alerts[alertKey].display = false;\n    }\n\n    onCloseClick() {\n        this.props.onClose();\n    }\n\n    onCodeChange(code) {\n        this.tempCode = code;\n        if (\"onCodeChange\" in this.props) {\n            this.props.onCodeChange({ ...toRaw(this.state._codeChanges) });\n        }\n    }\n\n    onSaveClick() {\n        if (!this.tempCode) {\n            return;\n        }\n        const resource = this.getResourceFromId(this.state.currentResourceId);\n        this.props.onSave({\n            resourceId: resource.id,\n            newCode: this.tempCode,\n            oldCode: resource.oldArch,\n        });\n    }\n\n    onResourceChange(resourceId) {\n        this.state.currentResourceId = resourceId;\n        this.props.onResourceChange(this.getResourceFromId(this.state.currentResourceId));\n    }\n\n    async loadResources(resourceId) {\n        const resources = await rpc(\"/web_studio/get_xml_editor_resources\", {\n            key: resourceId,\n        });\n\n        const resourcesOptions = resources.views.map((res) => ({\n            label: `${res.name} (${res.xml_id})`,\n            value: res.id,\n            resource: {\n                ...res,\n                oldArch: res.arch,\n                isMainResource:\n                    res.key === resourceId || res.id === resourceId || res.xml_id === resourceId,\n            },\n        }));\n\n        this.state.resourcesOptions = resourcesOptions;\n        if (resourcesOptions.length >= 1) {\n            let defaultResource = this.props.getDefaultResource(\n                resourcesOptions,\n                resources.main_view_key\n            );\n            if (!defaultResource && (this.props.defaultResourceId || resources.main_view_key)) {\n                const defaultId = this.props.defaultResourceId || resources.main_view_key;\n                defaultResource = resourcesOptions.find(\n                    (opt) =>\n                        opt.resource.id === defaultId ||\n                        opt.resource.xml_id === defaultId ||\n                        opt.resource.key === defaultId\n                );\n            }\n            defaultResource = defaultResource || resourcesOptions[0];\n            this.state.currentResourceId = defaultResource.value;\n        }\n\n        return resourcesOptions;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\n\nexport class NewReportDialog extends Component {\n    static template = \"web_studio.NewReportDialog\";\n    static components = { Dialog };\n    static props = [\"resModel\", \"onReportCreated\", \"close\"];\n\n    setup() {\n        this.layouts = [\n            {\n                name: \"web.external_layout\",\n                label: _t(\"External\"),\n                description: _t(\"Business header/footer\"),\n            },\n            {\n                name: \"web.internal_layout\",\n                label: _t(\"Internal\"),\n                description: _t(\"Minimal header/footer\"),\n            },\n            {\n                name: \"web.basic_layout\",\n                label: _t(\"Blank\"),\n                description: _t(\"No header/footer\"),\n            },\n        ];\n    }\n\n    async createNewReport(layout) {\n        const report = await rpc(\"/web_studio/create_new_report\", {\n            model_name: this.props.resModel,\n            layout,\n            context: user.context,\n        });\n        this.props.onReportCreated(report);\n        this.props.close();\n    }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\n\nimport { NewReportDialog } from \"./new_report_dialog\";\n\nclass StudioReportKanbanController extends KanbanController {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n    createRecord() {\n        this.dialogService.add(NewReportDialog, {\n            resModel: this.props.context.default_model,\n            onReportCreated: (report) => {\n                this.openRecord({ data: report, resId: report.id });\n            },\n        });\n    }\n\n    openRecord(record) {\n        return this.actionService.doAction(\"web_studio.action_edit_report\", {\n            report: {\n                data: record.data,\n                res_id: record.resId,\n            },\n        });\n    }\n}\n\nconst studioReportKanbanView = {\n    ...kanbanView,\n    Controller: StudioReportKanbanController,\n};\n\nregistry.category(\"views\").add(\"studio_report_kanban\", studioReportKanbanView);\n", "import { markup } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst MODEL_PAGE_HELP = _t(markup(`\n    <p class=\"o_view_nocontent_empty_folder\">\n        Create a new model page\n    </p>\n    <p>\n        Publish everything on your websites\n        then customize the pages\n        using the power of the Website app\n    </p>\n`));\n\nregistry.category(\"web_studio.editor_tabs\").add(\"website\", {\n    name: _t(\"Model Pages\"),\n    action: (env) => {\n        const { editedAction } = env.services.studio;\n        const context = {\n            default_model: editedAction.res_model,\n            search_default_model: editedAction.res_model,\n            default_page_type: \"listing\",\n            default_website_published: true,\n            default_use_menu: true,\n            default_auto_single_page: true,\n            form_view_ref: \"website_studio.website_controller_page_form_dialog\",\n            \"website_studio.create_page\": true,\n        }\n        return {\n            type: \"ir.actions.act_window\",\n            res_model: \"website.controller.page\",\n            name: _t(\"Model Pages\"),\n            views: [[false, \"kanban\"], [false, \"list\"], [false, \"form\"]],\n            context,\n            help: MODEL_PAGE_HELP,\n        }\n    },\n});\n"], "file": "/web/assets/796cb46/web_studio.studio_assets.js", "sourceRoot": "../../../"}